main repo

This commit is contained in:
Basilosaurusrex
2025-11-24 18:09:40 +01:00
parent b636ee5e70
commit f027651f9b
34146 changed files with 4436636 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
/* eslint-disable import/prefer-default-export, no-underscore-dangle */
import * as axe from 'axe-core';
export function axeFailMessage(checkId, data) {
return axe.utils.getCheckMessage(checkId, 'fail', data);
}

View File

@@ -0,0 +1,9 @@
import { version } from 'eslint/package.json';
import semver from 'semver';
const isESLintV8 = semver.major(version) >= 8;
// eslint-disable-next-line global-require, import/no-dynamic-require, import/no-unresolved
const getESLintCoreRule = (ruleId) => (isESLintV8 ? require('eslint/use-at-your-own-risk').builtinRules.get(ruleId) : require(`eslint/lib/rules/${ruleId}`));
export default getESLintCoreRule;

View File

@@ -0,0 +1,186 @@
import path from 'path';
import semver from 'semver';
import entries from 'object.entries';
import { version } from 'eslint/package.json';
import flatMap from 'array.prototype.flatmap';
let tsParserVersion;
try {
// eslint-disable-next-line import/no-unresolved, global-require
tsParserVersion = require('@typescript-eslint/parser/package.json').version;
} catch (e) { /**/ }
const disableNewTS = semver.satisfies(tsParserVersion, '>= 4.1') // this rule is not useful on v4.1+ of the TS parser
? (x) => ({ ...x, features: [].concat(x.features, 'no-ts-new') })
: (x) => x;
function minEcmaVersion(features, parserOptions) {
const minEcmaVersionForFeatures = {
'class fields': 2022,
'optional chaining': 2020,
'nullish coalescing': 2020,
};
const result = Math.max(
...[].concat(
(parserOptions && parserOptions.ecmaVersion) || [],
flatMap(entries(minEcmaVersionForFeatures), (entry) => {
const f = entry[0];
const y = entry[1];
return features.has(f) ? y : [];
}),
).map((y) => (y > 5 && y < 2015 ? y + 2009 : y)), // normalize editions to years
);
return Number.isFinite(result) ? result : undefined;
}
const NODE_MODULES = '../../node_modules';
const parsers = {
BABEL_ESLINT: path.join(__dirname, NODE_MODULES, 'babel-eslint'),
'@BABEL_ESLINT': path.join(__dirname, NODE_MODULES, '@babel/eslint-parser'),
TYPESCRIPT_ESLINT: path.join(__dirname, NODE_MODULES, 'typescript-eslint-parser'),
'@TYPESCRIPT_ESLINT': path.join(__dirname, NODE_MODULES, '@typescript-eslint/parser'),
disableNewTS,
babelParserOptions: function parserOptions(test, features) {
return {
...test.parserOptions,
requireConfigFile: false,
babelOptions: {
presets: [
'@babel/preset-react',
],
plugins: [
'@babel/plugin-syntax-do-expressions',
'@babel/plugin-syntax-function-bind',
['@babel/plugin-syntax-decorators', { legacy: true }],
],
parserOpts: {
allowSuperOutsideMethod: false,
allowReturnOutsideFunction: false,
},
},
ecmaFeatures: {
...test.parserOptions && test.parserOptions.ecmaFeatures,
jsx: true,
modules: true,
legacyDecorators: features.has('decorators'),
},
};
},
all: function all(tests) {
const t = flatMap(tests, (test) => {
/* eslint no-param-reassign: 0 */
if (typeof test === 'string') {
test = { code: test };
}
if ('parser' in test) {
delete test.features;
return test;
}
const features = new Set([].concat(test.features || []));
delete test.features;
const es = minEcmaVersion(features, test.parserOptions);
function addComment(testObject, parser) {
const extras = [].concat(
`features: [${Array.from(features).join(',')}]`,
`parser: ${parser}`,
testObject.parserOptions ? `parserOptions: ${JSON.stringify(testObject.parserOptions)}` : [],
testObject.options ? `options: ${JSON.stringify(testObject.options)}` : [],
testObject.settings ? `settings: ${JSON.stringify(testObject.settings)}` : [],
);
const extraComment = `\n// ${extras.join(', ')}`;
// Augment expected fix code output with extraComment
const nextCode = { code: testObject.code + extraComment };
const nextOutput = testObject.output && { output: testObject.output + extraComment };
// Augment expected suggestion outputs with extraComment
// `errors` may be a number (expected number of errors) or an array of
// error objects.
const nextErrors = testObject.errors
&& typeof testObject.errors !== 'number'
&& {
errors: testObject.errors.map(
(errorObject) => {
const nextSuggestions = errorObject.suggestions && {
suggestions: errorObject.suggestions.map((suggestion) => ({ ...suggestion, output: suggestion.output + extraComment })),
};
return { ...errorObject, ...nextSuggestions };
},
),
};
return {
...testObject,
...nextCode,
...nextOutput,
...nextErrors,
};
}
const skipBase = (features.has('class fields') && semver.satisfies(version, '< 8'))
|| (es >= 2020 && semver.satisfies(version, '< 6'))
|| features.has('no-default')
|| features.has('bind operator')
|| features.has('do expressions')
|| features.has('decorators')
|| features.has('flow')
|| features.has('ts')
|| features.has('types')
|| (features.has('fragment') && semver.satisfies(version, '< 5'));
const skipBabel = features.has('no-babel');
const skipOldBabel = skipBabel
|| features.has('no-babel-old')
|| features.has('optional chaining')
|| semver.satisfies(version, '>= 8');
const skipNewBabel = skipBabel
|| features.has('no-babel-new')
|| !semver.satisfies(version, '^7.5.0') // require('@babel/eslint-parser/package.json').peerDependencies.eslint
|| features.has('flow')
|| features.has('types')
|| features.has('ts');
const skipTS = semver.satisfies(version, '<= 5') // TODO: make these pass on eslint 5
|| features.has('no-ts')
|| features.has('flow')
|| features.has('jsx namespace')
|| features.has('bind operator')
|| features.has('do expressions');
const tsOld = !skipTS && !features.has('no-ts-old');
const tsNew = !skipTS && !features.has('no-ts-new');
return [].concat(
skipBase ? [] : addComment(
{
...test,
...typeof es === 'number' && {
parserOptions: { ...test.parserOptions, ecmaVersion: es },
},
},
'default',
),
skipOldBabel ? [] : addComment({
...test,
parser: parsers.BABEL_ESLINT,
parserOptions: parsers.babelParserOptions(test, features),
}, 'babel-eslint'),
skipNewBabel ? [] : addComment({
...test,
parser: parsers['@BABEL_ESLINT'],
parserOptions: parsers.babelParserOptions(test, features),
}, '@babel/eslint-parser'),
tsOld ? addComment({ ...test, parser: parsers.TYPESCRIPT_ESLINT }, 'typescript-eslint') : [],
tsNew ? addComment({ ...test, parser: parsers['@TYPESCRIPT_ESLINT'] }, '@typescript-eslint/parser') : [],
);
});
return t;
},
};
export default parsers;

View File

@@ -0,0 +1,53 @@
import { version as eslintVersion } from 'eslint/package.json';
import semver from 'semver';
const usingLegacy = semver.major(eslintVersion) < 9;
const defaultParserOptions = {
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
};
const defaultLegacyParserOptions = {
...defaultParserOptions,
ecmaVersion: 2018,
};
const defaultLanguageOptions = {
ecmaVersion: 'latest',
parserOptions: {
...defaultParserOptions,
},
};
export default function parserOptionsMapper({
code,
errors,
options = [],
languageOptions = {},
settings = {},
}) {
return usingLegacy
? {
code,
errors,
options,
parserOptions: {
...defaultLegacyParserOptions,
...languageOptions,
},
settings,
}
: {
code,
errors,
options,
languageOptions: {
...defaultLanguageOptions,
...languageOptions,
},
settings,
};
}

View File

@@ -0,0 +1,33 @@
/**
* @flow
*/
import entries from 'object.entries';
import flatMap from 'array.prototype.flatmap';
import fromEntries from 'object.fromentries';
type ESLintTestRunnerTestCase = {
code: string,
errors: ?Array<{ message: string, type: string }>,
options: ?Array<mixed>,
parserOptions: ?Array<mixed>,
settings?: {[string]: mixed},
};
type RuleOptionsMapperFactoryType = (
params: ESLintTestRunnerTestCase
) => ESLintTestRunnerTestCase;
export default function ruleOptionsMapperFactory(ruleOptions: Array<mixed> = []): RuleOptionsMapperFactoryType {
// eslint-disable-next-line
return ({ code, errors, options, parserOptions, settings }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => {
return {
code,
errors,
// Flatten the array of objects in an array of one object.
options: [fromEntries(flatMap((options || []).concat(ruleOptions), (item) => entries(item)))],
parserOptions,
settings,
};
};
}

View File

@@ -0,0 +1,40 @@
/* eslint global-require: 0 */
import fs from 'fs';
import path from 'path';
import test from 'tape';
import plugin from '../src';
const rules = fs.readdirSync(path.resolve(__dirname, '../src/rules/'))
.map((f) => path.basename(f, '.js'));
test('all rule files should be exported by the plugin', (t) => {
rules.forEach((ruleName) => {
t.equal(
plugin.rules[ruleName],
require(path.join('../src/rules', ruleName)), // eslint-disable-line import/no-dynamic-require
`exports ${ruleName}`,
);
});
t.end();
});
test('configurations', (t) => {
t.notEqual(plugin.configs.recommended, undefined, 'exports a \'recommended\' configuration');
t.end();
});
test('schemas', (t) => {
rules.forEach((ruleName) => {
const rule = require(path.join('../src/rules', ruleName)); // eslint-disable-line import/no-dynamic-require
const schema = rule.meta && rule.meta.schema && rule.meta.schema[0];
const { type } = schema;
t.equal(type, 'object', `${ruleName} exports a schema with type object`);
});
t.end();
});

View File

@@ -0,0 +1,66 @@
/**
* @fileoverview Enforce <marquee> elements are not used.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/accessible-emoji';
import parsers from '../../__util__/helpers/parsers';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Emojis should be wrapped in <span>, have role="img", and have an accessible description with aria-label or aria-labelledby.',
type: 'JSXOpeningElement',
};
ruleTester.run('accessible-emoji', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<span />' },
{ code: '<span>No emoji here!</span>' },
{ code: '<span role="img" aria-label="Panda face">🐼</span>' },
{ code: '<span role="img" aria-label="Snowman">&#9731;</span>' },
{ code: '<span role="img" aria-labelledby="id1">🐼</span>' },
{ code: '<span role="img" aria-labelledby="id1">&#9731;</span>' },
{ code: '<span role="img" aria-labelledby="id1" aria-label="Snowman">&#9731;</span>' },
{ code: '<span>{props.emoji}</span>' },
{ code: '<span aria-hidden>{props.emoji}</span>' },
{ code: '<span aria-hidden="true">🐼</span>' },
{ code: '<span aria-hidden>🐼</span>' },
{ code: '<div aria-hidden="true">🐼</div>' },
{ code: '<input type="hidden">🐼</input>' },
{
code: '<CustomInput type="hidden">🐼</CustomInput>',
settings: { 'jsx-a11y': { components: { CustomInput: 'input' } } },
},
{
code: '<Box as="input" type="hidden">🐼</Box>',
settings: { 'jsx-a11y': { polymorphicPropName: 'as' } },
},
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<span>🐼</span>', errors: [expectedError] },
{ code: '<span>foo🐼bar</span>', errors: [expectedError] },
{ code: '<span>foo 🐼 bar</span>', errors: [expectedError] },
{ code: '<i role="img" aria-label="Panda face">🐼</i>', errors: [expectedError] },
{ code: '<i role="img" aria-labelledby="id1">🐼</i>', errors: [expectedError] },
{ code: '<Foo>🐼</Foo>', errors: [expectedError] },
{ code: '<span aria-hidden="false">🐼</span>', errors: [expectedError] },
{ code: '<CustomInput type="hidden">🐼</CustomInput>', errors: [expectedError] },
{
code: '<Box as="span">🐼</Box>',
settings: { 'jsx-a11y': { polymorphicPropName: 'as' } },
errors: [expectedError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,291 @@
/**
* @fileoverview Enforce all elements that require alternative text have it.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/alt-text';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const missingPropError = (type) => ({
message: `${type} elements must have an alt prop, either with meaningful text, or an empty string for decorative images.`,
type: 'JSXOpeningElement',
});
const altValueError = (type) => ({
message: `Invalid alt value for ${type}. \
Use alt="" for presentational images.`,
type: 'JSXOpeningElement',
});
const ariaLabelValueError = {
message: 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.',
};
const ariaLabelledbyValueError = {
message: 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.',
};
const preferAltError = () => ({
message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.',
type: 'JSXOpeningElement',
});
const objectError = {
message: 'Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.',
};
const areaError = {
message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
};
const inputImageError = {
message: '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
};
const componentsSettings = {
'jsx-a11y': {
polymorphicPropName: 'as',
components: {
Input: 'input',
},
},
};
const array = [{
img: ['Thumbnail', 'Image'],
object: ['Object'],
area: ['Area'],
'input[type="image"]': ['InputImage'],
}];
ruleTester.run('alt-text', rule, {
valid: parsers.all([].concat(
// DEFAULT ELEMENT 'img' TESTS
{ code: '<img alt="foo" />;' },
{ code: '<img alt={"foo"} />;' },
{ code: '<img alt={alt} />;' },
{ code: '<img ALT="foo" />;' },
{ code: '<img ALT={`This is the ${alt} text`} />;' },
{ code: '<img ALt="foo" />;' },
{ code: '<img alt="foo" salt={undefined} />;' },
{ code: '<img {...this.props} alt="foo" />' },
{ code: '<a />' },
{ code: '<div />' },
{ code: '<img alt={function(e) {} } />' },
{ code: '<div alt={function(e) {} } />' },
{ code: '<img alt={() => void 0} />' },
{ code: '<IMG />' },
{ code: '<UX.Layout>test</UX.Layout>' },
{ code: '<img alt={alt || "Alt text" } />' },
{ code: '<img alt={photo.caption} />;' },
{ code: '<img alt={bar()} />;' },
{ code: '<img alt={foo.bar || ""} />' },
{ code: '<img alt={bar() || ""} />' },
{ code: '<img alt={foo.bar() || ""} />' },
{ code: '<img alt="" />' },
{ code: '<img alt={`${undefined}`} />' },
{ code: '<img alt=" " />' },
{ code: '<img alt="" role="presentation" />' },
{ code: '<img alt="" role="none" />' },
{ code: '<img alt="" role={`presentation`} />' },
{ code: '<img alt="" role={"presentation"} />' },
{ code: '<img alt="this is lit..." role="presentation" />' },
{ code: '<img alt={error ? "not working": "working"} />' },
{ code: '<img alt={undefined ? "working": "not working"} />' },
{ code: '<img alt={plugin.name + " Logo"} />' },
{ code: '<img aria-label="foo" />' },
{ code: '<img aria-labelledby="id1" />' },
// DEFAULT <object> TESTS
{ code: '<object aria-label="foo" />' },
{ code: '<object aria-labelledby="id1" />' },
{ code: '<object>Foo</object>' },
{ code: '<object><p>This is descriptive!</p></object>' },
{ code: '<Object />' },
{ code: '<object title="An object" />' },
// DEFAULT <area> TESTS
{ code: '<area aria-label="foo" />' },
{ code: '<area aria-labelledby="id1" />' },
{ code: '<area alt="" />' },
{ code: '<area alt="This is descriptive!" />' },
{ code: '<area alt={altText} />' },
{ code: '<Area />' },
// DEFAULT <input type="image"> TESTS
{ code: '<input />' },
{ code: '<input type="foo" />' },
{ code: '<input type="image" aria-label="foo" />' },
{ code: '<input type="image" aria-labelledby="id1" />' },
{ code: '<input type="image" alt="" />' },
{ code: '<input type="image" alt="This is descriptive!" />' },
{ code: '<input type="image" alt={altText} />' },
{ code: '<InputImage />' },
{ code: '<Input type="image" alt="" />', settings: componentsSettings },
{ code: '<SomeComponent as="input" type="image" alt="" />', settings: componentsSettings },
// CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS
{ code: '<Thumbnail alt="foo" />;', options: array },
{ code: '<Thumbnail alt={"foo"} />;', options: array },
{ code: '<Thumbnail alt={alt} />;', options: array },
{ code: '<Thumbnail ALT="foo" />;', options: array },
{ code: '<Thumbnail ALT={`This is the ${alt} text`} />;', options: array },
{ code: '<Thumbnail ALt="foo" />;', options: array },
{ code: '<Thumbnail alt="foo" salt={undefined} />;', options: array },
{ code: '<Thumbnail {...this.props} alt="foo" />', options: array },
{ code: '<thumbnail />', options: array },
{ code: '<Thumbnail alt={function(e) {} } />', options: array },
{ code: '<div alt={function(e) {} } />', options: array },
{ code: '<Thumbnail alt={() => void 0} />', options: array },
{ code: '<THUMBNAIL />', options: array },
{ code: '<Thumbnail alt={alt || "foo" } />', options: array },
{ code: '<Image alt="foo" />;', options: array },
{ code: '<Image alt={"foo"} />;', options: array },
{ code: '<Image alt={alt} />;', options: array },
{ code: '<Image ALT="foo" />;', options: array },
{ code: '<Image ALT={`This is the ${alt} text`} />;', options: array },
{ code: '<Image ALt="foo" />;', options: array },
{ code: '<Image alt="foo" salt={undefined} />;', options: array },
{ code: '<Image {...this.props} alt="foo" />', options: array },
{ code: '<image />', options: array },
{ code: '<Image alt={function(e) {} } />', options: array },
{ code: '<div alt={function(e) {} } />', options: array },
{ code: '<Image alt={() => void 0} />', options: array },
{ code: '<IMAGE />', options: array },
{ code: '<Image alt={alt || "foo" } />', options: array },
{ code: '<Object aria-label="foo" />', options: array },
{ code: '<Object aria-labelledby="id1" />', options: array },
{ code: '<Object>Foo</Object>', options: array },
{ code: '<Object><p>This is descriptive!</p></Object>', options: array },
{ code: '<Object title="An object" />', options: array },
{ code: '<Area aria-label="foo" />', options: array },
{ code: '<Area aria-labelledby="id1" />', options: array },
{ code: '<Area alt="" />', options: array },
{ code: '<Area alt="This is descriptive!" />', options: array },
{ code: '<Area alt={altText} />', options: array },
{ code: '<InputImage aria-label="foo" />', options: array },
{ code: '<InputImage aria-labelledby="id1" />', options: array },
{ code: '<InputImage alt="" />', options: array },
{ code: '<InputImage alt="This is descriptive!" />', options: array },
{ code: '<InputImage alt={altText} />', options: array },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// DEFAULT ELEMENT 'img' TESTS
{ code: '<img />;', errors: [missingPropError('img')] },
{ code: '<img alt />;', errors: [altValueError('img')] },
{ code: '<img alt={undefined} />;', errors: [altValueError('img')] },
{ code: '<img src="xyz" />', errors: [missingPropError('img')] },
{ code: '<img role />', errors: [missingPropError('img')] },
{ code: '<img {...this.props} />', errors: [missingPropError('img')] },
{ code: '<img alt={false || false} />', errors: [altValueError('img')] },
{ code: '<img alt={undefined} role="presentation" />;', errors: [altValueError('img')] },
{ code: '<img alt role="presentation" />;', errors: [altValueError('img')] },
{ code: '<img role="presentation" />;', errors: [preferAltError()] },
{ code: '<img role="none" />;', errors: [preferAltError()] },
{ code: '<img aria-label={undefined} />', errors: [ariaLabelValueError] },
{ code: '<img aria-labelledby={undefined} />', errors: [ariaLabelledbyValueError] },
{ code: '<img aria-label="" />', errors: [ariaLabelValueError] },
{ code: '<img aria-labelledby="" />', errors: [ariaLabelledbyValueError] },
{ code: '<SomeComponent as="img" aria-label="" />', settings: componentsSettings, errors: [ariaLabelValueError] },
// DEFAULT ELEMENT 'object' TESTS
{ code: '<object />', errors: [objectError] },
{ code: '<object><div aria-hidden /></object>', errors: [objectError] },
{ code: '<object title={undefined} />', errors: [objectError] },
{ code: '<object aria-label="" />', errors: [objectError] },
{ code: '<object aria-labelledby="" />', errors: [objectError] },
{ code: '<object aria-label={undefined} />', errors: [objectError] },
{ code: '<object aria-labelledby={undefined} />', errors: [objectError] },
// DEFAULT ELEMENT 'area' TESTS
{ code: '<area />', errors: [areaError] },
{ code: '<area alt />', errors: [areaError] },
{ code: '<area alt={undefined} />', errors: [areaError] },
{ code: '<area src="xyz" />', errors: [areaError] },
{ code: '<area {...this.props} />', errors: [areaError] },
{ code: '<area aria-label="" />', errors: [areaError] },
{ code: '<area aria-label={undefined} />', errors: [areaError] },
{ code: '<area aria-labelledby="" />', errors: [areaError] },
{ code: '<area aria-labelledby={undefined} />', errors: [areaError] },
// DEFAULT ELEMENT 'input type="image"' TESTS
{ code: '<input type="image" />', errors: [inputImageError] },
{ code: '<input type="image" alt />', errors: [inputImageError] },
{ code: '<input type="image" alt={undefined} />', errors: [inputImageError] },
{ code: '<input type="image">Foo</input>', errors: [inputImageError] },
{ code: '<input type="image" {...this.props} />', errors: [inputImageError] },
{ code: '<input type="image" aria-label="" />', errors: [inputImageError] },
{ code: '<input type="image" aria-label={undefined} />', errors: [inputImageError] },
{ code: '<input type="image" aria-labelledby="" />', errors: [inputImageError] },
{ code: '<input type="image" aria-labelledby={undefined} />', errors: [inputImageError] },
// CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS
{
code: '<Thumbnail />;',
errors: [missingPropError('Thumbnail')],
options: array,
},
{
code: '<Thumbnail alt />;',
errors: [altValueError('Thumbnail')],
options: array,
},
{
code: '<Thumbnail alt={undefined} />;',
errors: [altValueError('Thumbnail')],
options: array,
},
{
code: '<Thumbnail src="xyz" />',
errors: [missingPropError('Thumbnail')],
options: array,
},
{
code: '<Thumbnail {...this.props} />',
errors: [missingPropError('Thumbnail')],
options: array,
},
{ code: '<Image />;', errors: [missingPropError('Image')], options: array },
{ code: '<Image alt />;', errors: [altValueError('Image')], options: array },
{
code: '<Image alt={undefined} />;',
errors: [altValueError('Image')],
options: array,
},
{
code: '<Image src="xyz" />',
errors: [missingPropError('Image')],
options: array,
},
{
code: '<Image {...this.props} />',
errors: [missingPropError('Image')],
options: array,
},
{ code: '<Object />', errors: [objectError], options: array },
{ code: '<Object><div aria-hidden /></Object>', errors: [objectError], options: array },
{ code: '<Object title={undefined} />', errors: [objectError], options: array },
{ code: '<Area />', errors: [areaError], options: array },
{ code: '<Area alt />', errors: [areaError], options: array },
{ code: '<Area alt={undefined} />', errors: [areaError], options: array },
{ code: '<Area src="xyz" />', errors: [areaError], options: array },
{ code: '<Area {...this.props} />', errors: [areaError], options: array },
{ code: '<InputImage />', errors: [inputImageError], options: array },
{ code: '<InputImage alt />', errors: [inputImageError], options: array },
{ code: '<InputImage alt={undefined} />', errors: [inputImageError], options: array },
{ code: '<InputImage>Foo</InputImage>', errors: [inputImageError], options: array },
{ code: '<InputImage {...this.props} />', errors: [inputImageError], options: array },
{ code: '<Input type="image" />', errors: [inputImageError], settings: componentsSettings },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,117 @@
/**
* @fileoverview Enforce `<a>` text to not exactly match "click here", "here", "link", or "a link".
* @author Matt Wang
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/anchor-ambiguous-text';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const DEFAULT_AMBIGUOUS_WORDS = [
'click here',
'here',
'link',
'a link',
'learn more',
];
const expectedErrorGenerator = (words) => ({
message: `Ambiguous text within anchor. Screenreader users rely on link text for context; the words "${words.join('", "')}" are ambiguous and do not provide enough context.`,
type: 'JSXOpeningElement',
});
const expectedError = expectedErrorGenerator(DEFAULT_AMBIGUOUS_WORDS);
ruleTester.run('anchor-ambiguous-text', rule, {
valid: parsers.all([].concat(
{ code: '<a>documentation</a>;' },
{ code: '<a>${here}</a>;' },
{ code: '<a aria-label="tutorial on using eslint-plugin-jsx-a11y">click here</a>;' },
{ code: '<a><span aria-label="tutorial on using eslint-plugin-jsx-a11y">click here</span></a>;' },
{ code: '<a><img alt="documentation" /></a>;' },
{
code: '<a>click here</a>',
options: [{
words: ['disabling the defaults'],
}],
},
{
code: '<Link>documentation</Link>;',
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
{
code: '<a><Image alt="documentation" /></a>;',
settings: { 'jsx-a11y': { components: { Image: 'img' } } },
},
{
code: '<Link>${here}</Link>;',
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
{
code: '<Link aria-label="tutorial on using eslint-plugin-jsx-a11y">click here</Link>;',
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
{
code: '<Link>click here</Link>',
options: [{
words: ['disabling the defaults with components'],
}],
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<a>here</a>;', errors: [expectedError] },
{ code: '<a>HERE</a>;', errors: [expectedError] },
{ code: '<a>click here</a>;', errors: [expectedError] },
{ code: '<a>learn more</a>;', errors: [expectedError] },
{ code: '<a>learn more</a>;', errors: [expectedError] },
{ code: '<a>learn more.</a>;', errors: [expectedError] },
{ code: '<a>learn more?</a>;', errors: [expectedError] },
{ code: '<a>learn more,</a>;', errors: [expectedError] },
{ code: '<a>learn more!</a>;', errors: [expectedError] },
{ code: '<a>learn more;</a>;', errors: [expectedError] },
{ code: '<a>learn more:</a>;', errors: [expectedError] },
{ code: '<a>link</a>;', errors: [expectedError] },
{ code: '<a>a link</a>;', errors: [expectedError] },
{ code: '<a aria-label="click here">something</a>;', errors: [expectedError] },
{ code: '<a> a link </a>;', errors: [expectedError] },
{ code: '<a>a<i></i> link</a>;', errors: [expectedError] },
{ code: '<a><i></i>a link</a>;', errors: [expectedError] },
{ code: '<a><span>click</span> here</a>;', errors: [expectedError] },
{ code: '<a><span> click </span> here</a>;', errors: [expectedError] },
{ code: '<a><span aria-hidden>more text</span>learn more</a>;', errors: [expectedError] },
{ code: '<a><span aria-hidden="true">more text</span>learn more</a>;', errors: [expectedError] },
{ code: '<a><img alt="click here"/></a>;', errors: [expectedError] },
{ code: '<a alt="tutorial on using eslint-plugin-jsx-a11y">click here</a>;', errors: [expectedError] },
{ code: '<a><span alt="tutorial on using eslint-plugin-jsx-a11y">click here</span></a>;', errors: [expectedError] },
{ code: '<a><CustomElement>click</CustomElement> here</a>;', errors: [expectedError] },
{
code: '<Link>here</Link>',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
{
code: '<a><Image alt="click here" /></a>',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { Image: 'img' } } },
},
{
code: '<a>a disallowed word</a>',
errors: [expectedErrorGenerator(['a disallowed word'])],
options: [{
words: ['a disallowed word'],
}],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,54 @@
/**
* @fileoverview Enforce anchor elements to contain accessible content.
* @author Lisa Ring & Niklas Holmberg
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/anchor-has-content';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Anchors must have content and the content must be accessible by a screen reader.',
type: 'JSXOpeningElement',
};
ruleTester.run('anchor-has-content', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<a>Foo</a>' },
{ code: '<a><Bar /></a>' },
{ code: '<a>{foo}</a>' },
{ code: '<a>{foo.bar}</a>' },
{ code: '<a dangerouslySetInnerHTML={{ __html: "foo" }} />' },
{ code: '<a children={children} />' },
{ code: '<Link />' },
{
code: '<Link>foo</Link>',
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
{ code: '<a title={title} />' },
{ code: '<a aria-label={ariaLabel} />' },
{ code: '<a title={title} aria-label={ariaLabel} />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<a />', errors: [expectedError] },
{ code: '<a><Bar aria-hidden /></a>', errors: [expectedError] },
{ code: '<a>{undefined}</a>', errors: [expectedError] },
{
code: '<Link />',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,532 @@
/**
* @fileoverview Performs validity check on anchor hrefs. Warns when anchors are used as buttons.
* @author Almero Steyn
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/anchor-is-valid';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const preferButtonErrorMessage = 'Anchor used as a button. Anchors are primarily expected to navigate. Use the button element instead. Learn more: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/docs/rules/anchor-is-valid.md';
const noHrefErrorMessage = 'The href attribute is required for an anchor to be keyboard accessible. Provide a valid, navigable address as the href value. If you cannot provide an href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/docs/rules/anchor-is-valid.md';
const invalidHrefErrorMessage = 'The href attribute requires a valid value to be accessible. Provide a valid, navigable address as the href value. If you cannot provide a valid href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/docs/rules/anchor-is-valid.md';
const preferButtonexpectedError = {
message: preferButtonErrorMessage,
type: 'JSXOpeningElement',
};
const noHrefexpectedError = {
message: noHrefErrorMessage,
type: 'JSXOpeningElement',
};
const invalidHrefexpectedError = {
message: invalidHrefErrorMessage,
type: 'JSXOpeningElement',
};
const components = [{
components: ['Anchor', 'Link'],
}];
const specialLink = [{
specialLink: ['hrefLeft', 'hrefRight'],
}];
const noHrefAspect = [{
aspects: ['noHref'],
}];
const invalidHrefAspect = [{
aspects: ['invalidHref'],
}];
const preferButtonAspect = [{
aspects: ['preferButton'],
}];
const noHrefInvalidHrefAspect = [{
aspects: ['noHref', 'invalidHref'],
}];
const noHrefPreferButtonAspect = [{
aspects: ['noHref', 'preferButton'],
}];
const preferButtonInvalidHrefAspect = [{
aspects: ['preferButton', 'invalidHref'],
}];
const componentsAndSpecialLink = [{
components: ['Anchor'],
specialLink: ['hrefLeft'],
}];
const componentsAndSpecialLinkAndInvalidHrefAspect = [{
components: ['Anchor'],
specialLink: ['hrefLeft'],
aspects: ['invalidHref'],
}];
const componentsAndSpecialLinkAndNoHrefAspect = [{
components: ['Anchor'],
specialLink: ['hrefLeft'],
aspects: ['noHref'],
}];
const componentsSettings = {
'jsx-a11y': {
components: {
Anchor: 'a',
Link: 'a',
},
},
};
ruleTester.run('anchor-is-valid', rule, {
valid: parsers.all([].concat(
// DEFAULT ELEMENT 'a' TESTS
{ code: '<Anchor />' },
{ code: '<a {...props} />' },
{ code: '<a href="foo" />' },
{ code: '<a href={foo} />' },
{ code: '<a href="/foo" />' },
{ code: '<a href="https://foo.bar.com" />' },
{ code: '<div href="foo" />' },
{ code: '<a href="javascript" />' },
{ code: '<a href="javascriptFoo" />' },
{ code: '<a href={`#foo`}/>' },
{ code: '<a href={"foo"}/>' },
{ code: '<a href={"javascript"}/>' },
{ code: '<a href={`#javascript`}/>' },
{ code: '<a href="#foo" />' },
{ code: '<a href="#javascript" />' },
{ code: '<a href="#javascriptFoo" />' },
{ code: '<UX.Layout>test</UX.Layout>' },
{ code: '<a href={this} />' },
// CUSTOM ELEMENT TEST FOR ARRAY OPTION
{ code: '<Anchor {...props} />', options: components },
{ code: '<Anchor href="foo" />', options: components },
{ code: '<Anchor href={foo} />', options: components },
{ code: '<Anchor href="/foo" />', options: components },
{ code: '<Anchor href="https://foo.bar.com" />', options: components },
{ code: '<div href="foo" />', options: components },
{ code: '<Anchor href={`#foo`}/>', options: components },
{ code: '<Anchor href={"foo"}/>', options: components },
{ code: '<Anchor href="#foo" />', options: components },
{ code: '<Link {...props} />', options: components },
{ code: '<Link href="foo" />', options: components },
{ code: '<Link href={foo} />', options: components },
{ code: '<Link href="/foo" />', options: components },
{ code: '<Link href="https://foo.bar.com" />', options: components },
{ code: '<div href="foo" />', options: components },
{ code: '<Link href={`#foo`}/>', options: components },
{ code: '<Link href={"foo"}/>', options: components },
{ code: '<Link href="#foo" />', options: components },
{ code: '<Link href="#foo" />', settings: componentsSettings },
// CUSTOM PROP TESTS
{ code: '<a {...props} />', options: specialLink },
{ code: '<a hrefLeft="foo" />', options: specialLink },
{ code: '<a hrefLeft={foo} />', options: specialLink },
{ code: '<a hrefLeft="/foo" />', options: specialLink },
{ code: '<a hrefLeft="https://foo.bar.com" />', options: specialLink },
{ code: '<div hrefLeft="foo" />', options: specialLink },
{ code: '<a hrefLeft={`#foo`}/>', options: specialLink },
{ code: '<a hrefLeft={"foo"}/>', options: specialLink },
{ code: '<a hrefLeft="#foo" />', options: specialLink },
{ code: '<UX.Layout>test</UX.Layout>', options: specialLink },
{ code: '<a hrefRight={this} />', options: specialLink },
{ code: '<a {...props} />', options: specialLink },
{ code: '<a hrefRight="foo" />', options: specialLink },
{ code: '<a hrefRight={foo} />', options: specialLink },
{ code: '<a hrefRight="/foo" />', options: specialLink },
{ code: '<a hrefRight="https://foo.bar.com" />', options: specialLink },
{ code: '<div hrefRight="foo" />', options: specialLink },
{ code: '<a hrefRight={`#foo`}/>', options: specialLink },
{ code: '<a hrefRight={"foo"}/>', options: specialLink },
{ code: '<a hrefRight="#foo" />', options: specialLink },
{ code: '<UX.Layout>test</UX.Layout>', options: specialLink },
{ code: '<a hrefRight={this} />', options: specialLink },
// CUSTOM BOTH COMPONENTS AND SPECIALLINK TESTS
{ code: '<Anchor {...props} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="foo" />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={foo} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="/foo" />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="https://foo.bar.com" />', options: componentsAndSpecialLink },
{ code: '<div hrefLeft="foo" />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={`#foo`}/>', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={"foo"}/>', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="#foo" />', options: componentsAndSpecialLink },
{ code: '<UX.Layout>test</UX.Layout>', options: componentsAndSpecialLink },
// WITH ONCLICK
// DEFAULT ELEMENT 'a' TESTS
{ code: '<a {...props} onClick={() => void 0} />' },
{ code: '<a href="foo" onClick={() => void 0} />' },
{ code: '<a href={foo} onClick={() => void 0} />' },
{ code: '<a href="/foo" onClick={() => void 0} />' },
{ code: '<a href="https://foo.bar.com" onClick={() => void 0} />' },
{ code: '<div href="foo" onClick={() => void 0} />' },
{ code: '<a href={`#foo`} onClick={() => void 0} />' },
{ code: '<a href={"foo"} onClick={() => void 0} />' },
{ code: '<a href="#foo" onClick={() => void 0} />' },
{ code: '<a href={this} onClick={() => void 0} />' },
// CUSTOM ELEMENT TEST FOR ARRAY OPTION
{ code: '<Anchor {...props} onClick={() => void 0} />', options: components },
{ code: '<Anchor href="foo" onClick={() => void 0} />', options: components },
{ code: '<Anchor href={foo} onClick={() => void 0} />', options: components },
{ code: '<Anchor href="/foo" onClick={() => void 0} />', options: components },
{ code: '<Anchor href="https://foo.bar.com" onClick={() => void 0} />', options: components },
{ code: '<Anchor href={`#foo`} onClick={() => void 0} />', options: components },
{ code: '<Anchor href={"foo"} onClick={() => void 0} />', options: components },
{ code: '<Anchor href="#foo" onClick={() => void 0} />', options: components },
{ code: '<Link {...props} onClick={() => void 0} />', options: components },
{ code: '<Link href="foo" onClick={() => void 0} />', options: components },
{ code: '<Link href={foo} onClick={() => void 0} />', options: components },
{ code: '<Link href="/foo" onClick={() => void 0} />', options: components },
{ code: '<Link href="https://foo.bar.com" onClick={() => void 0} />', options: components },
{ code: '<div href="foo" onClick={() => void 0} />', options: components },
{ code: '<Link href={`#foo`} onClick={() => void 0} />', options: components },
{ code: '<Link href={"foo"} onClick={() => void 0} />', options: components },
{ code: '<Link href="#foo" onClick={() => void 0} />', options: components },
// CUSTOM PROP TESTS
{ code: '<a {...props} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft="foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft={foo} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft="/foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft href="https://foo.bar.com" onClick={() => void 0} />', options: specialLink },
{ code: '<div hrefLeft="foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft={`#foo`} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft={"foo"} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft="#foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight={this} onClick={() => void 0} />', options: specialLink },
{ code: '<a {...props} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight="foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight={foo} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight="/foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight href="https://foo.bar.com" onClick={() => void 0} />', options: specialLink },
{ code: '<div hrefRight="foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight={`#foo`} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight={"foo"} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight="#foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight={this} onClick={() => void 0} />', options: specialLink },
// CUSTOM BOTH COMPONENTS AND SPECIALLINK TESTS
{ code: '<Anchor {...props} onClick={() => void 0} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="foo" onClick={() => void 0} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={foo} onClick={() => void 0} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="/foo" onClick={() => void 0} />', options: componentsAndSpecialLink },
{
code: '<Anchor hrefLeft href="https://foo.bar.com" onClick={() => void 0} />',
options: componentsAndSpecialLink,
},
{ code: '<Anchor hrefLeft={`#foo`} onClick={() => void 0} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={"foo"} onClick={() => void 0} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="#foo" onClick={() => void 0} />', options: componentsAndSpecialLink },
// WITH ASPECTS TESTS
// NO HREF
{ code: '<a />', options: invalidHrefAspect },
{ code: '<a href={undefined} />', options: invalidHrefAspect },
{ code: '<a href={null} />', options: invalidHrefAspect },
{ code: '<a />', options: preferButtonAspect },
{ code: '<a href={undefined} />', options: preferButtonAspect },
{ code: '<a href={null} />', options: preferButtonAspect },
{ code: '<a />', options: preferButtonInvalidHrefAspect },
{ code: '<a href={undefined} />', options: preferButtonInvalidHrefAspect },
{ code: '<a href={null} />', options: preferButtonInvalidHrefAspect },
// INVALID HREF
{ code: '<a href="" />;', options: preferButtonAspect },
{ code: '<a href="#" />', options: preferButtonAspect },
{ code: '<a href={"#"} />', options: preferButtonAspect },
{ code: '<a href="javascript:void(0)" />', options: preferButtonAspect },
{ code: '<a href={"javascript:void(0)"} />', options: preferButtonAspect },
{ code: '<a href="" />;', options: noHrefAspect },
{ code: '<a href="#" />', options: noHrefAspect },
{ code: '<a href={"#"} />', options: noHrefAspect },
{ code: '<a href="javascript:void(0)" />', options: noHrefAspect },
{ code: '<a href={"javascript:void(0)"} />', options: noHrefAspect },
{ code: '<a href="" />;', options: noHrefPreferButtonAspect },
{ code: '<a href="#" />', options: noHrefPreferButtonAspect },
{ code: '<a href={"#"} />', options: noHrefPreferButtonAspect },
{ code: '<a href="javascript:void(0)" />', options: noHrefPreferButtonAspect },
{ code: '<a href={"javascript:void(0)"} />', options: noHrefPreferButtonAspect },
// SHOULD BE BUTTON
{ code: '<a onClick={() => void 0} />', options: invalidHrefAspect },
{ code: '<a href="#" onClick={() => void 0} />', options: noHrefAspect },
{ code: '<a href="javascript:void(0)" onClick={() => void 0} />', options: noHrefAspect },
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: noHrefAspect,
},
// CUSTOM COMPONENTS AND SPECIAL LINK AND ASPECT
{ code: '<Anchor hrefLeft={undefined} />', options: componentsAndSpecialLinkAndInvalidHrefAspect },
{ code: '<Anchor hrefLeft={null} />', options: componentsAndSpecialLinkAndInvalidHrefAspect },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// DEFAULT ELEMENT 'a' TESTS
// NO HREF
{ code: '<a />', errors: [noHrefexpectedError] },
{ code: '<a href={undefined} />', errors: [noHrefexpectedError] },
{ code: '<a href={null} />', errors: [noHrefexpectedError] },
// INVALID HREF
{ code: '<a href="" />;', errors: [invalidHrefexpectedError] },
{ code: '<a href="#" />', errors: [invalidHrefexpectedError] },
{ code: '<a href={"#"} />', errors: [invalidHrefexpectedError] },
{ code: '<a href="javascript:void(0)" />', errors: [invalidHrefexpectedError] },
{ code: '<a href={"javascript:void(0)"} />', errors: [invalidHrefexpectedError] },
// SHOULD BE BUTTON
{ code: '<a onClick={() => void 0} />', errors: [preferButtonexpectedError] },
{ code: '<a href="#" onClick={() => void 0} />', errors: [preferButtonexpectedError] },
{ code: '<a href="javascript:void(0)" onClick={() => void 0} />', errors: [preferButtonexpectedError] },
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
errors: [preferButtonexpectedError],
},
// CUSTOM ELEMENT TEST FOR ARRAY OPTION
// NO HREF
{ code: '<Link />', errors: [noHrefexpectedError], options: components },
{ code: '<Link href={undefined} />', errors: [noHrefexpectedError], options: components },
{ code: '<Link href={null} />', errors: [noHrefexpectedError], options: components },
// INVALID HREF
{ code: '<Link href="" />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Link href="#" />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Link href={"#"} />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Link href="javascript:void(0)" />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Link href={"javascript:void(0)"} />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Anchor href="" />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Anchor href="#" />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Anchor href={"#"} />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Anchor href="javascript:void(0)" />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Anchor href={"javascript:void(0)"} />', errors: [invalidHrefexpectedError], options: components },
// SHOULD BE BUTTON
{ code: '<Link onClick={() => void 0} />', errors: [preferButtonexpectedError], options: components },
{ code: '<Link href="#" onClick={() => void 0} />', errors: [preferButtonexpectedError], options: components },
{
code: '<Link href="javascript:void(0)" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: components,
},
{
code: '<Link href={"javascript:void(0)"} onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: components,
},
{ code: '<Anchor onClick={() => void 0} />', errors: [preferButtonexpectedError], options: components },
{ code: '<Anchor href="#" onClick={() => void 0} />', errors: [preferButtonexpectedError], options: components },
{
code: '<Anchor href="javascript:void(0)" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: components,
},
{
code: '<Anchor href={"javascript:void(0)"} onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: components,
},
{
code: '<Link href="#" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
settings: componentsSettings,
},
// CUSTOM PROP TESTS
// NO HREF
{ code: '<a hrefLeft={undefined} />', errors: [noHrefexpectedError], options: specialLink },
{ code: '<a hrefLeft={null} />', errors: [noHrefexpectedError], options: specialLink },
// INVALID HREF
{ code: '<a hrefLeft="" />;', errors: [invalidHrefexpectedError], options: specialLink },
{ code: '<a hrefLeft="#" />', errors: [invalidHrefexpectedError], options: specialLink },
{ code: '<a hrefLeft={"#"} />', errors: [invalidHrefexpectedError], options: specialLink },
{ code: '<a hrefLeft="javascript:void(0)" />', errors: [invalidHrefexpectedError], options: specialLink },
{ code: '<a hrefLeft={"javascript:void(0)"} />', errors: [invalidHrefexpectedError], options: specialLink },
// SHOULD BE BUTTON
{ code: '<a hrefLeft="#" onClick={() => void 0} />', errors: [preferButtonexpectedError], options: specialLink },
{
code: '<a hrefLeft="javascript:void(0)" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: specialLink,
},
{
code: '<a hrefLeft={"javascript:void(0)"} onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: specialLink,
},
// CUSTOM BOTH COMPONENTS AND SPECIAL LINK TESTS
// NO HREF
{ code: '<Anchor Anchor={undefined} />', errors: [noHrefexpectedError], options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={null} />', errors: [noHrefexpectedError], options: componentsAndSpecialLink },
// INVALID HREF
{ code: '<Anchor hrefLeft="" />;', errors: [invalidHrefexpectedError], options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="#" />', errors: [invalidHrefexpectedError], options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={"#"} />', errors: [invalidHrefexpectedError], options: componentsAndSpecialLink },
{
code: '<Anchor hrefLeft="javascript:void(0)" />',
errors: [invalidHrefexpectedError],
options: componentsAndSpecialLink,
},
{
code: '<Anchor hrefLeft={"javascript:void(0)"} />',
errors: [invalidHrefexpectedError],
options: componentsAndSpecialLink,
},
// SHOULD BE BUTTON
{
code: '<Anchor hrefLeft="#" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: componentsAndSpecialLink,
},
{
code: '<Anchor hrefLeft="javascript:void(0)" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: componentsAndSpecialLink,
},
{
code: '<Anchor hrefLeft={"javascript:void(0)"} onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: componentsAndSpecialLink,
},
// WITH ASPECTS TESTS
// NO HREF
{ code: '<a />', options: noHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a />', options: noHrefPreferButtonAspect, errors: [noHrefexpectedError] },
{ code: '<a />', options: noHrefInvalidHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a href={undefined} />', options: noHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a href={undefined} />', options: noHrefPreferButtonAspect, errors: [noHrefexpectedError] },
{ code: '<a href={undefined} />', options: noHrefInvalidHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a href={null} />', options: noHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a href={null} />', options: noHrefPreferButtonAspect, errors: [noHrefexpectedError] },
{ code: '<a href={null} />', options: noHrefInvalidHrefAspect, errors: [noHrefexpectedError] },
// INVALID HREF
{ code: '<a href="" />;', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="" />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="" />;', options: preferButtonInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="#" />;', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="#" />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="#" />;', options: preferButtonInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href={"#"} />;', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href={"#"} />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href={"#"} />;', options: preferButtonInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="javascript:void(0)" />;', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="javascript:void(0)" />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{
code: '<a href="javascript:void(0)" />;',
options: preferButtonInvalidHrefAspect,
errors: [invalidHrefexpectedError],
},
{ code: '<a href={"javascript:void(0)"} />;', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href={"javascript:void(0)"} />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{
code: '<a href={"javascript:void(0)"} />;',
options: preferButtonInvalidHrefAspect,
errors: [invalidHrefexpectedError],
},
// SHOULD BE BUTTON
{ code: '<a onClick={() => void 0} />', options: preferButtonAspect, errors: [preferButtonexpectedError] },
{
code: '<a onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonexpectedError],
},
{ code: '<a onClick={() => void 0} />', options: noHrefPreferButtonAspect, errors: [preferButtonexpectedError] },
{ code: '<a onClick={() => void 0} />', options: noHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a onClick={() => void 0} />', options: noHrefInvalidHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a href="#" onClick={() => void 0} />', options: preferButtonAspect, errors: [preferButtonexpectedError] },
{
code: '<a href="#" onClick={() => void 0} />',
options: noHrefPreferButtonAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href="#" onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonexpectedError],
},
{ code: '<a href="#" onClick={() => void 0} />', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{
code: '<a href="#" onClick={() => void 0} />',
options: noHrefInvalidHrefAspect,
errors: [invalidHrefexpectedError],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: preferButtonAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: noHrefPreferButtonAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: invalidHrefAspect,
errors: [invalidHrefexpectedError],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: noHrefInvalidHrefAspect,
errors: [invalidHrefexpectedError],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: preferButtonAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: noHrefPreferButtonAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: invalidHrefAspect,
errors: [invalidHrefexpectedError],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: noHrefInvalidHrefAspect,
errors: [invalidHrefexpectedError],
},
// CUSTOM COMPONENTS AND SPECIAL LINK AND ASPECT
{
code: '<Anchor hrefLeft={undefined} />',
options: componentsAndSpecialLinkAndNoHrefAspect,
errors: [noHrefexpectedError],
},
{
code: '<Anchor hrefLeft={null} />',
options: componentsAndSpecialLinkAndNoHrefAspect,
errors: [noHrefexpectedError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,95 @@
/**
* @fileoverview Enforce elements with aria-activedescendant are tabbable.
* @author Jesse Beach <@jessebeach>
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/aria-activedescendant-has-tabindex';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'An element that manages focus with `aria-activedescendant` must have a tabindex',
type: 'JSXOpeningElement',
};
ruleTester.run('aria-activedescendant-has-tabindex', rule, {
valid: parsers.all([].concat(
{
code: '<CustomComponent />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} tabIndex={0} />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} tabIndex={-1} />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} tabIndex={0} />;',
settings: { 'jsx-a11y': { components: { CustomComponent: 'div' } } },
},
{
code: '<div />;',
},
{
code: '<input />;',
},
{
code: '<div tabIndex={0} />;',
},
{
code: '<div aria-activedescendant={someID} tabIndex={0} />;',
},
{
code: '<div aria-activedescendant={someID} tabIndex="0" />;',
},
{
code: '<div aria-activedescendant={someID} tabIndex={1} />;',
},
{
code: '<input aria-activedescendant={someID} />;',
},
{
code: '<input aria-activedescendant={someID} tabIndex={1} />;',
},
{
code: '<input aria-activedescendant={someID} tabIndex={0} />;',
},
{
code: '<input aria-activedescendant={someID} tabIndex={-1} />;',
},
{
code: '<div aria-activedescendant={someID} tabIndex={-1} />;',
},
{
code: '<div aria-activedescendant={someID} tabIndex="-1" />;',
},
{
code: '<input aria-activedescendant={someID} tabIndex={-1} />;',
},
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{
code: '<div aria-activedescendant={someID} />;',
errors: [expectedError],
},
{
code: '<CustomComponent aria-activedescendant={someID} />;',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { CustomComponent: 'div' } } },
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,69 @@
/**
* @fileoverview Enforce all aria-* properties are valid.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { aria } from 'aria-query';
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/aria-props';
import getSuggestion from '../../../src/util/getSuggestion';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const ariaAttributes = [...aria.keys()];
const errorMessage = (name) => {
const suggestions = getSuggestion(name, ariaAttributes);
const message = `${name}: This attribute is an invalid ARIA attribute.`;
if (suggestions.length > 0) {
return {
type: 'JSXAttribute',
message: `${message} Did you mean to use ${suggestions}?`,
};
}
return {
type: 'JSXAttribute',
message,
};
};
// Create basic test cases using all valid role types.
const basicValidityTests = ariaAttributes.map((prop) => ({
code: `<div ${prop.toLowerCase()}="foobar" />`,
}));
ruleTester.run('aria-props', rule, {
valid: parsers.all([].concat(
// Variables should pass, as we are only testing literals.
{ code: '<div />' },
{ code: '<div></div>' },
{ code: '<div aria="wee"></div>' }, // Needs aria-*
{ code: '<div abcARIAdef="true"></div>' },
{ code: '<div fooaria-foobar="true"></div>' },
{ code: '<div fooaria-hidden="true"></div>' },
{ code: '<Bar baz />' },
{ code: '<input type="text" aria-errormessage="foobar" />' },
)).concat(basicValidityTests).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div aria-="foobar" />', errors: [errorMessage('aria-')] },
{
code: '<div aria-labeledby="foobar" />',
errors: [errorMessage('aria-labeledby')],
},
{
code: '<div aria-skldjfaria-klajsd="foobar" />',
errors: [errorMessage('aria-skldjfaria-klajsd')],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,311 @@
/**
* @fileoverview Enforce ARIA state and property values are valid.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { aria } from 'aria-query';
import { RuleTester } from 'eslint';
import test from 'tape';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/aria-proptypes';
const { validityCheck } = rule;
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = (name) => {
const {
type,
values: permittedValues,
} = aria.get(name.toLowerCase());
switch (type) {
case 'tristate':
return { message: `The value for ${name} must be a boolean or the string "mixed".` };
case 'token':
return { message: `The value for ${name} must be a single token from the following: ${permittedValues}.` };
case 'tokenlist':
return {
message: `The value for ${name} must be a list of one or more \
tokens from the following: ${permittedValues}.`,
};
case 'idlist':
return { message: `The value for ${name} must be a list of strings that represent DOM element IDs (idlist)` };
case 'id':
return { message: `The value for ${name} must be a string that represents a DOM element ID` };
case 'boolean':
case 'string':
case 'integer':
case 'number':
default:
return { message: `The value for ${name} must be a ${type}.` };
}
};
test('validityCheck', (t) => {
t.equal(
validityCheck(null, null),
false,
'is false for an unknown expected type',
);
t.end();
});
ruleTester.run('aria-proptypes', rule, {
valid: parsers.all([].concat(
// DON'T TEST INVALID ARIA-* PROPS
{ code: '<div aria-foo="true" />' },
{ code: '<div abcaria-foo="true" />' },
// BOOLEAN
{ code: '<div aria-hidden={true} />' },
{ code: '<div aria-hidden="true" />' },
{ code: '<div aria-hidden={"false"} />' },
{ code: '<div aria-hidden={!false} />' },
{ code: '<div aria-hidden />' },
{ code: '<div aria-hidden={false} />' },
{ code: '<div aria-hidden={!true} />' },
{ code: '<div aria-hidden={!"yes"} />' },
{ code: '<div aria-hidden={foo} />' },
{ code: '<div aria-hidden={foo.bar} />' },
{ code: '<div aria-hidden={null} />' },
{ code: '<div aria-hidden={undefined} />' },
{ code: '<div aria-hidden={<div />} />' },
// STRING
{ code: '<div aria-label="Close" />' },
{ code: '<div aria-label={`Close`} />' },
{ code: '<div aria-label={foo} />' },
{ code: '<div aria-label={foo.bar} />' },
{ code: '<div aria-label={null} />' },
{ code: '<div aria-label={undefined} />' },
{ code: '<input aria-invalid={error ? "true" : "false"} />' },
{ code: '<input aria-invalid={undefined ? "true" : "false"} />' },
// TRISTATE
{ code: '<div aria-checked={true} />' },
{ code: '<div aria-checked="true" />' },
{ code: '<div aria-checked={"false"} />' },
{ code: '<div aria-checked={!false} />' },
{ code: '<div aria-checked />' },
{ code: '<div aria-checked={false} />' },
{ code: '<div aria-checked={!true} />' },
{ code: '<div aria-checked={!"yes"} />' },
{ code: '<div aria-checked={foo} />' },
{ code: '<div aria-checked={foo.bar} />' },
{ code: '<div aria-checked="mixed" />' },
{ code: '<div aria-checked={`mixed`} />' },
{ code: '<div aria-checked={null} />' },
{ code: '<div aria-checked={undefined} />' },
// INTEGER
{ code: '<div aria-level={123} />' },
{ code: '<div aria-level={-123} />' },
{ code: '<div aria-level={+123} />' },
{ code: '<div aria-level={~123} />' },
{ code: '<div aria-level={"123"} />' },
{ code: '<div aria-level={`123`} />' },
{ code: '<div aria-level="123" />' },
{ code: '<div aria-level={foo} />' },
{ code: '<div aria-level={foo.bar} />' },
{ code: '<div aria-level={null} />' },
{ code: '<div aria-level={undefined} />' },
// NUMBER
{ code: '<div aria-valuemax={123} />' },
{ code: '<div aria-valuemax={-123} />' },
{ code: '<div aria-valuemax={+123} />' },
{ code: '<div aria-valuemax={~123} />' },
{ code: '<div aria-valuemax={"123"} />' },
{ code: '<div aria-valuemax={`123`} />' },
{ code: '<div aria-valuemax="123" />' },
{ code: '<div aria-valuemax={foo} />' },
{ code: '<div aria-valuemax={foo.bar} />' },
{ code: '<div aria-valuemax={null} />' },
{ code: '<div aria-valuemax={undefined} />' },
// TOKEN
{ code: '<div aria-sort="ascending" />' },
{ code: '<div aria-sort="ASCENDING" />' },
{ code: '<div aria-sort={"ascending"} />' },
{ code: '<div aria-sort={`ascending`} />' },
{ code: '<div aria-sort="descending" />' },
{ code: '<div aria-sort={"descending"} />' },
{ code: '<div aria-sort={`descending`} />' },
{ code: '<div aria-sort="none" />' },
{ code: '<div aria-sort={"none"} />' },
{ code: '<div aria-sort={`none`} />' },
{ code: '<div aria-sort="other" />' },
{ code: '<div aria-sort={"other"} />' },
{ code: '<div aria-sort={`other`} />' },
{ code: '<div aria-sort={foo} />' },
{ code: '<div aria-sort={foo.bar} />' },
{ code: '<div aria-invalid={true} />' },
{ code: '<div aria-invalid="true" />' },
{ code: '<div aria-invalid={false} />' },
{ code: '<div aria-invalid="false" />' },
{ code: '<div aria-invalid="grammar" />' },
{ code: '<div aria-invalid="spelling" />' },
{ code: '<div aria-invalid={null} />' },
{ code: '<div aria-invalid={undefined} />' },
// TOKENLIST
{ code: '<div aria-relevant="additions" />' },
{ code: '<div aria-relevant={"additions"} />' },
{ code: '<div aria-relevant={`additions`} />' },
{ code: '<div aria-relevant="additions removals" />' },
{ code: '<div aria-relevant="additions additions" />' },
{ code: '<div aria-relevant={"additions removals"} />' },
{ code: '<div aria-relevant={`additions removals`} />' },
{ code: '<div aria-relevant="additions removals text" />' },
{ code: '<div aria-relevant={"additions removals text"} />' },
{ code: '<div aria-relevant={`additions removals text`} />' },
{ code: '<div aria-relevant="additions removals text all" />' },
{ code: '<div aria-relevant={"additions removals text all"} />' },
{ code: '<div aria-relevant={`removals additions text all`} />' },
{ code: '<div aria-relevant={foo} />' },
{ code: '<div aria-relevant={foo.bar} />' },
{ code: '<div aria-relevant={null} />' },
{ code: '<div aria-relevant={undefined} />' },
// ID
{ code: '<div aria-activedescendant="ascending" />' },
{ code: '<div aria-activedescendant="ASCENDING" />' },
{ code: '<div aria-activedescendant={"ascending"} />' },
{ code: '<div aria-activedescendant={`ascending`} />' },
{ code: '<div aria-activedescendant="descending" />' },
{ code: '<div aria-activedescendant={"descending"} />' },
{ code: '<div aria-activedescendant={`descending`} />' },
{ code: '<div aria-activedescendant="none" />' },
{ code: '<div aria-activedescendant={"none"} />' },
{ code: '<div aria-activedescendant={`none`} />' },
{ code: '<div aria-activedescendant="other" />' },
{ code: '<div aria-activedescendant={"other"} />' },
{ code: '<div aria-activedescendant={`other`} />' },
{ code: '<div aria-activedescendant={foo} />' },
{ code: '<div aria-activedescendant={foo.bar} />' },
{ code: '<div aria-activedescendant={null} />' },
{ code: '<div aria-activedescendant={undefined} />' },
// IDLIST
{ code: '<div aria-labelledby="additions" />' },
{ code: '<div aria-labelledby={"additions"} />' },
{ code: '<div aria-labelledby={`additions`} />' },
{ code: '<div aria-labelledby="additions removals" />' },
{ code: '<div aria-labelledby="additions additions" />' },
{ code: '<div aria-labelledby={"additions removals"} />' },
{ code: '<div aria-labelledby={`additions removals`} />' },
{ code: '<div aria-labelledby="additions removals text" />' },
{ code: '<div aria-labelledby={"additions removals text"} />' },
{ code: '<div aria-labelledby={`additions removals text`} />' },
{ code: '<div aria-labelledby="additions removals text all" />' },
{ code: '<div aria-labelledby={"additions removals text all"} />' },
{ code: '<div aria-labelledby={`removals additions text all`} />' },
{ code: '<div aria-labelledby={foo} />' },
{ code: '<div aria-labelledby={foo.bar} />' },
{ code: '<div aria-labelledby={null} />' },
{ code: '<div aria-labelledby={undefined} />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// BOOLEAN
{ code: '<div aria-hidden="yes" />', errors: [errorMessage('aria-hidden')] },
{ code: '<div aria-hidden="no" />', errors: [errorMessage('aria-hidden')] },
{ code: '<div aria-hidden={1234} />', errors: [errorMessage('aria-hidden')] },
{
code: '<div aria-hidden={`${abc}`} />',
errors: [errorMessage('aria-hidden')],
},
// STRING
{ code: '<div aria-label />', errors: [errorMessage('aria-label')] },
{ code: '<div aria-label={true} />', errors: [errorMessage('aria-label')] },
{ code: '<div aria-label={false} />', errors: [errorMessage('aria-label')] },
{ code: '<div aria-label={1234} />', errors: [errorMessage('aria-label')] },
{ code: '<div aria-label={!true} />', errors: [errorMessage('aria-label')] },
// TRISTATE
{ code: '<div aria-checked="yes" />', errors: [errorMessage('aria-checked')] },
{ code: '<div aria-checked="no" />', errors: [errorMessage('aria-checked')] },
{ code: '<div aria-checked={1234} />', errors: [errorMessage('aria-checked')] },
{
code: '<div aria-checked={`${abc}`} />',
errors: [errorMessage('aria-checked')],
},
// INTEGER
{ code: '<div aria-level="yes" />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level="no" />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level={`abc`} />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level={true} />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level={"false"} />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level={!"false"} />', errors: [errorMessage('aria-level')] },
// NUMBER
{ code: '<div aria-valuemax="yes" />', errors: [errorMessage('aria-valuemax')] },
{ code: '<div aria-valuemax="no" />', errors: [errorMessage('aria-valuemax')] },
{
code: '<div aria-valuemax={`abc`} />',
errors: [errorMessage('aria-valuemax')],
},
{
code: '<div aria-valuemax={true} />',
errors: [errorMessage('aria-valuemax')],
},
{ code: '<div aria-valuemax />', errors: [errorMessage('aria-valuemax')] },
{
code: '<div aria-valuemax={"false"} />',
errors: [errorMessage('aria-valuemax')],
},
{
code: '<div aria-valuemax={!"false"} />',
errors: [errorMessage('aria-valuemax')],
},
// TOKEN
{ code: '<div aria-sort="" />', errors: [errorMessage('aria-sort')] },
{ code: '<div aria-sort="descnding" />', errors: [errorMessage('aria-sort')] },
{ code: '<div aria-sort />', errors: [errorMessage('aria-sort')] },
{ code: '<div aria-sort={true} />', errors: [errorMessage('aria-sort')] },
{ code: '<div aria-sort={"false"} />', errors: [errorMessage('aria-sort')] },
{
code: '<div aria-sort="ascending descending" />',
errors: [errorMessage('aria-sort')],
},
// TOKENLIST
{ code: '<div aria-relevant="" />', errors: [errorMessage('aria-relevant')] },
{
code: '<div aria-relevant="foobar" />',
errors: [errorMessage('aria-relevant')],
},
{ code: '<div aria-relevant />', errors: [errorMessage('aria-relevant')] },
{
code: '<div aria-relevant={true} />',
errors: [errorMessage('aria-relevant')],
},
{
code: '<div aria-relevant={"false"} />',
errors: [errorMessage('aria-relevant')],
},
{
code: '<div aria-relevant="additions removalss" />',
errors: [errorMessage('aria-relevant')],
},
{
code: '<div aria-relevant="additions removalss " />',
errors: [errorMessage('aria-relevant')],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,119 @@
/**
* @fileoverview Enforce aria role attribute is valid.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { roles } from 'aria-query';
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/aria-role';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = {
message: 'Elements with ARIA roles must use a valid, non-abstract ARIA role.',
type: 'JSXAttribute',
};
const roleKeys = [...roles.keys()];
const validRoles = roleKeys.filter((role) => roles.get(role).abstract === false);
const invalidRoles = roleKeys.filter((role) => roles.get(role).abstract === true);
const createTests = (roleNames) => roleNames.map((role) => ({
code: `<div role="${role.toLowerCase()}" />`,
}));
const validTests = createTests(validRoles);
const invalidTests = createTests(invalidRoles).map((test) => {
const invalidTest = { ...test };
invalidTest.errors = [errorMessage];
return invalidTest;
});
const allowedInvalidRoles = [{
allowedInvalidRoles: ['invalid-role', 'other-invalid-role'],
}];
const ignoreNonDOMSchema = [{
ignoreNonDOM: true,
}];
const customDivSettings = {
'jsx-a11y': {
polymorphicPropName: 'asChild',
components: {
Div: 'div',
},
},
};
ruleTester.run('aria-role', rule, {
valid: parsers.all([].concat(
// Variables should pass, as we are only testing literals.
{ code: '<div />' },
{ code: '<div></div>' },
{ code: '<div role={role} />' },
{ code: '<div role={role || "button"} />' },
{ code: '<div role={role || "foobar"} />' },
{ code: '<div role="tabpanel row" />' },
{ code: '<div role="switch" />' },
{ code: '<div role="doc-abstract" />' },
{ code: '<div role="doc-appendix doc-bibliography" />' },
{ code: '<Bar baz />' },
{ code: '<img role="invalid-role" />', options: allowedInvalidRoles },
{ code: '<img role="invalid-role tabpanel" />', options: allowedInvalidRoles },
{ code: '<img role="invalid-role other-invalid-role" />', options: allowedInvalidRoles },
{ code: '<Foo role="bar" />', options: ignoreNonDOMSchema },
{ code: '<fakeDOM role="bar" />', options: ignoreNonDOMSchema },
{ code: '<img role="presentation" />', options: ignoreNonDOMSchema },
{
code: '<Div role="button" />',
settings: customDivSettings,
},
{
code: '<Box asChild="div" role="button" />',
settings: customDivSettings,
},
{
code: '<svg role="graphics-document document" />',
},
)).concat(validTests).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div role="foobar" />', errors: [errorMessage] },
{ code: '<div role="datepicker"></div>', errors: [errorMessage] },
{ code: '<div role="range"></div>', errors: [errorMessage] },
{ code: '<div role="Button"></div>', errors: [errorMessage] },
{ code: '<div role=""></div>', errors: [errorMessage] },
{ code: '<div role="tabpanel row foobar"></div>', errors: [errorMessage] },
{ code: '<div role="tabpanel row range"></div>', errors: [errorMessage] },
{ code: '<div role="doc-endnotes range"></div>', errors: [errorMessage] },
{ code: '<div role />', errors: [errorMessage] },
{ code: '<div role="unknown-invalid-role" />', errors: [errorMessage], options: allowedInvalidRoles },
{ code: '<div role={null}></div>', errors: [errorMessage] },
{ code: '<Foo role="datepicker" />', errors: [errorMessage] },
{ code: '<Foo role="Button" />', errors: [errorMessage] },
{ code: '<Div role="Button" />', errors: [errorMessage], settings: customDivSettings },
{
code: '<Div role="Button" />',
errors: [errorMessage],
options: ignoreNonDOMSchema,
settings: customDivSettings,
},
{
code: '<Box asChild="div" role="Button" />',
settings: customDivSettings,
errors: [errorMessage],
},
)).concat(invalidTests).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,75 @@
/**
* @fileoverview Enforce that elements that do not support ARIA roles,
* states and properties do not have those attributes.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { dom } from 'aria-query';
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/aria-unsupported-elements';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = (invalidProp) => ({
message: `This element does not support ARIA roles, states and properties. \
Try removing the prop '${invalidProp}'.`,
type: 'JSXOpeningElement',
});
const domElements = [...dom.keys()];
// Generate valid test cases
const roleValidityTests = domElements.map((element) => {
const isReserved = dom.get(element).reserved || false;
const role = isReserved ? '' : 'role';
return {
code: `<${element} ${role} />`,
};
});
const ariaValidityTests = domElements.map((element) => {
const isReserved = dom.get(element).reserved || false;
const aria = isReserved ? '' : 'aria-hidden';
return {
code: `<${element} ${aria} />`,
};
}).concat({
code: '<fake aria-hidden />',
errors: [errorMessage('aria-hidden')],
});
// Generate invalid test cases.
const invalidRoleValidityTests = domElements
.filter((element) => dom.get(element).reserved)
.map((reservedElem) => ({
code: `<${reservedElem} role {...props} />`,
errors: [errorMessage('role')],
})).concat({
code: '<Meta aria-hidden />',
errors: [errorMessage('aria-hidden')],
settings: { 'jsx-a11y': { components: { Meta: 'meta' } } },
});
const invalidAriaValidityTests = domElements
.filter((element) => dom.get(element).reserved)
.map((reservedElem) => ({
code: `<${reservedElem} aria-hidden aria-role="none" {...props} />`,
errors: [errorMessage('aria-hidden')],
}));
ruleTester.run('aria-unsupported-elements', rule, {
valid: parsers.all([].concat(roleValidityTests, ariaValidityTests)).map(parserOptionsMapper),
invalid: parsers.all([].concat(invalidRoleValidityTests, invalidAriaValidityTests))
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,77 @@
/**
* @fileoverview Ensure autocomplete attribute is correct.
* @author Wilco Fiers
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import { axeFailMessage } from '../../__util__/axeMapping';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/autocomplete-valid';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const invalidAutocomplete = [{
message: axeFailMessage('autocomplete-valid'),
type: 'JSXOpeningElement',
}];
const inappropriateAutocomplete = [{
message: axeFailMessage('autocomplete-appropriate'),
type: 'JSXOpeningElement',
}];
const componentsSettings = {
'jsx-a11y': {
components: {
Input: 'input',
},
},
};
ruleTester.run('autocomplete-valid', rule, {
valid: parsers.all([].concat(
// INAPPLICABLE
{ code: '<input type="text" />;' },
// // PASSED AUTOCOMPLETE
{ code: '<input type="text" autocomplete="name" />;' },
{ code: '<input type="text" autocomplete="" />;' },
{ code: '<input type="text" autocomplete="off" />;' },
{ code: '<input type="text" autocomplete="on" />;' },
{ code: '<input type="text" autocomplete="billing family-name" />;' },
{ code: '<input type="text" autocomplete="section-blue shipping street-address" />;' },
{ code: '<input type="text" autocomplete="section-somewhere shipping work email" />;' },
{ code: '<input type="text" autocomplete />;' },
{ code: '<input type="text" autocomplete={autocompl} />;' },
{ code: '<input type="text" autocomplete={autocompl || "name"} />;' },
{ code: '<input type="text" autocomplete={autocompl || "foo"} />;' },
{ code: '<Foo autocomplete="bar"></Foo>;' },
{ code: '<input type={isEmail ? "email" : "text"} autocomplete="none" />;' },
{ code: '<Input type="text" autocomplete="name" />', settings: componentsSettings },
{ code: '<Input type="text" autocomplete="baz" />' },
// PASSED "autocomplete-appropriate"
// see also: https://github.com/dequelabs/axe-core/issues/2912
{ code: '<input type="date" autocomplete="email" />;', errors: inappropriateAutocomplete },
{ code: '<input type="number" autocomplete="url" />;', errors: inappropriateAutocomplete },
{ code: '<input type="month" autocomplete="tel" />;', errors: inappropriateAutocomplete },
{ code: '<Foo type="month" autocomplete="tel"></Foo>;', errors: inappropriateAutocomplete, options: [{ inputComponents: ['Foo'] }] },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// FAILED "autocomplete-valid"
{ code: '<input type="text" autocomplete="foo" />;', errors: invalidAutocomplete },
{ code: '<input type="text" autocomplete="name invalid" />;', errors: invalidAutocomplete },
{ code: '<input type="text" autocomplete="invalid name" />;', errors: invalidAutocomplete },
{ code: '<input type="text" autocomplete="home url" />;', errors: invalidAutocomplete },
{ code: '<Bar autocomplete="baz"></Bar>;', errors: invalidAutocomplete, options: [{ inputComponents: ['Bar'] }] },
{ code: '<Input type="text" autocomplete="baz" />', errors: invalidAutocomplete, settings: componentsSettings },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,77 @@
/**
* @fileoverview Enforce a clickable non-interactive element has at least 1 keyboard event listener.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/click-events-have-key-events';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = 'Visible, non-interactive elements with click handlers must have at least one keyboard listener.';
const expectedError = {
message: errorMessage,
type: 'JSXOpeningElement',
};
ruleTester.run('click-events-have-key-events', rule, {
valid: parsers.all([].concat(
{ code: '<div onClick={() => void 0} onKeyDown={foo}/>;' },
{ code: '<div onClick={() => void 0} onKeyUp={foo} />;' },
{ code: '<div onClick={() => void 0} onKeyPress={foo}/>;' },
{ code: '<div onClick={() => void 0} onKeyDown={foo} onKeyUp={bar} />;' },
{ code: '<div onClick={() => void 0} onKeyDown={foo} {...props} />;' },
{ code: '<div className="foo" />;' },
{ code: '<div onClick={() => void 0} aria-hidden />;' },
{ code: '<div onClick={() => void 0} aria-hidden={true} />;' },
{ code: '<div onClick={() => void 0} aria-hidden={false} onKeyDown={foo} />;' },
{
code: '<div onClick={() => void 0} onKeyDown={foo} aria-hidden={undefined} />;',
},
{ code: '<input type="text" onClick={() => void 0} />' },
{ code: '<input onClick={() => void 0} />' },
{ code: '<button onClick={() => void 0} className="foo" />' },
{ code: '<option onClick={() => void 0} className="foo" />' },
{ code: '<select onClick={() => void 0} className="foo" />' },
{ code: '<textarea onClick={() => void 0} className="foo" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
{ code: '<input onClick={() => void 0} type="hidden" />;' },
{ code: '<div onClick={() => void 0} role="presentation" />;' },
{ code: '<div onClick={() => void 0} role="none" />;' },
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
{ code: '<Footer onClick={doFoo} />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div onClick={() => void 0} />;', errors: [expectedError] },
{
code: '<div onClick={() => void 0} role={undefined} />;',
errors: [expectedError],
},
{ code: '<div onClick={() => void 0} {...props} />;', errors: [expectedError] },
{ code: '<section onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<main onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<article onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<header onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<footer onClick={() => void 0} />;', errors: [expectedError] },
{
code: '<div onClick={() => void 0} aria-hidden={false} />;',
errors: [expectedError],
},
{ code: '<a onClick={() => void 0} />', errors: [expectedError] },
{ code: '<a tabIndex="0" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<Footer onClick={doFoo} />', errors: [expectedError], settings: { 'jsx-a11y': { components: { Footer: 'footer' } } } },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,327 @@
/**
* @fileoverview Control elements must be associated with a text label
* @author jessebeach
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
import rule from '../../../src/rules/control-has-associated-label';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const ruleName = 'jsx-a11y/control-has-associated-label';
const expectedError = {
message: 'A control must be associated with a text label.',
type: 'JSXOpeningElement',
};
const alwaysValid = [
// Custom Control Components
{ code: '<CustomControl><span><span>Save</span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'] }] },
{ code: '<CustomControl><span><span label="Save"></span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'], labelAttributes: ['label'] }] },
{ code: '<CustomControl>Save</CustomControl>', settings: { 'jsx-a11y': { components: { CustomControl: 'button' } } } },
// Interactive Elements
{ code: '<button>Save</button>' },
{ code: '<button><span>Save</span></button>' },
{ code: '<button><span><span>Save</span></span></button>', options: [{ depth: 3 }] },
{ code: '<button><span><span><span><span><span><span><span><span>Save</span></span></span></span></span></span></span></span></button>', options: [{ depth: 9 }] },
{ code: '<button><img alt="Save" /></button>' },
{ code: '<button aria-label="Save" />' },
{ code: '<button><span aria-label="Save" /></button>' },
{ code: '<button aria-labelledby="js_1" />' },
{ code: '<button><span aria-labelledby="js_1" /></button>' },
{ code: '<button>{sureWhyNot}</button>' },
{ code: '<button><span><span label="Save"></span></span></button>', options: [{ depth: 3, labelAttributes: ['label'] }] },
{ code: '<a href="#">Save</a>' },
{ code: '<area href="#">Save</area>' },
{ code: '<link>Save</link>' },
{ code: '<menuitem>Save</menuitem>' },
{ code: '<option>Save</option>' },
{ code: '<th>Save</th>' },
// Interactive Roles
{ code: '<div role="button">Save</div>' },
{ code: '<div role="checkbox">Save</div>' },
{ code: '<div role="columnheader">Save</div>' },
{ code: '<div role="combobox">Save</div>' },
{ code: '<div role="gridcell">Save</div>' },
{ code: '<div role="link">Save</div>' },
{ code: '<div role="menuitem">Save</div>' },
{ code: '<div role="menuitemcheckbox">Save</div>' },
{ code: '<div role="menuitemradio">Save</div>' },
{ code: '<div role="option">Save</div>' },
{ code: '<div role="progressbar">Save</div>' },
{ code: '<div role="radio">Save</div>' },
{ code: '<div role="rowheader">Save</div>' },
{ code: '<div role="searchbox">Save</div>' },
{ code: '<div role="slider">Save</div>' },
{ code: '<div role="spinbutton">Save</div>' },
{ code: '<div role="switch">Save</div>' },
{ code: '<div role="tab">Save</div>' },
{ code: '<div role="textbox">Save</div>' },
{ code: '<div role="treeitem">Save</div>' },
{ code: '<div role="button" aria-label="Save" />' },
{ code: '<div role="checkbox" aria-label="Save" />' },
{ code: '<div role="columnheader" aria-label="Save" />' },
{ code: '<div role="combobox" aria-label="Save" />' },
{ code: '<div role="gridcell" aria-label="Save" />' },
{ code: '<div role="link" aria-label="Save" />' },
{ code: '<div role="menuitem" aria-label="Save" />' },
{ code: '<div role="menuitemcheckbox" aria-label="Save" />' },
{ code: '<div role="menuitemradio" aria-label="Save" />' },
{ code: '<div role="option" aria-label="Save" />' },
{ code: '<div role="progressbar" aria-label="Save" />' },
{ code: '<div role="radio" aria-label="Save" />' },
{ code: '<div role="rowheader" aria-label="Save" />' },
{ code: '<div role="searchbox" aria-label="Save" />' },
{ code: '<div role="slider" aria-label="Save" />' },
{ code: '<div role="spinbutton" aria-label="Save" />' },
{ code: '<div role="switch" aria-label="Save" />' },
{ code: '<div role="tab" aria-label="Save" />' },
{ code: '<div role="textbox" aria-label="Save" />' },
{ code: '<div role="treeitem" aria-label="Save" />' },
{ code: '<div role="button" aria-labelledby="js_1" />' },
{ code: '<div role="checkbox" aria-labelledby="js_1" />' },
{ code: '<div role="columnheader" aria-labelledby="js_1" />' },
{ code: '<div role="combobox" aria-labelledby="js_1" />' },
{ code: '<div role="gridcell" aria-labelledby="Save" />' },
{ code: '<div role="link" aria-labelledby="js_1" />' },
{ code: '<div role="menuitem" aria-labelledby="js_1" />' },
{ code: '<div role="menuitemcheckbox" aria-labelledby="js_1" />' },
{ code: '<div role="menuitemradio" aria-labelledby="js_1" />' },
{ code: '<div role="option" aria-labelledby="js_1" />' },
{ code: '<div role="progressbar" aria-labelledby="js_1" />' },
{ code: '<div role="radio" aria-labelledby="js_1" />' },
{ code: '<div role="rowheader" aria-labelledby="js_1" />' },
{ code: '<div role="searchbox" aria-labelledby="js_1" />' },
{ code: '<div role="slider" aria-labelledby="js_1" />' },
{ code: '<div role="spinbutton" aria-labelledby="js_1" />' },
{ code: '<div role="switch" aria-labelledby="js_1" />' },
{ code: '<div role="tab" aria-labelledby="js_1" />' },
{ code: '<div role="textbox" aria-labelledby="js_1" />' },
{ code: '<div role="treeitem" aria-labelledby="js_1" />' },
// Non-interactive Elements
{ code: '<abbr />' },
{ code: '<article />' },
{ code: '<blockquote />' },
{ code: '<br />' },
{ code: '<caption />' },
{ code: '<dd />' },
{ code: '<details />' },
{ code: '<dfn />' },
{ code: '<dialog />' },
{ code: '<dir />' },
{ code: '<dl />' },
{ code: '<dt />' },
{ code: '<fieldset />' },
{ code: '<figcaption />' },
{ code: '<figure />' },
{ code: '<footer />' },
{ code: '<form />' },
{ code: '<frame />' },
{ code: '<h1 />' },
{ code: '<h2 />' },
{ code: '<h3 />' },
{ code: '<h4 />' },
{ code: '<h5 />' },
{ code: '<h6 />' },
{ code: '<hr />' },
{ code: '<iframe />' },
{ code: '<img />' },
{ code: '<label />' },
{ code: '<legend />' },
{ code: '<li />' },
{ code: '<link />' },
{ code: '<main />' },
{ code: '<mark />' },
{ code: '<marquee />' },
{ code: '<menu />' },
{ code: '<meter />' },
{ code: '<nav />' },
{ code: '<ol />' },
{ code: '<p />' },
{ code: '<pre />' },
{ code: '<progress />' },
{ code: '<ruby />' },
{ code: '<section />' },
{ code: '<table />' },
{ code: '<tbody />' },
{ code: '<td />' },
{ code: '<tfoot />' },
{ code: '<thead />' },
{ code: '<time />' },
{ code: '<ul />' },
// Non-interactive Roles
{ code: '<div role="alert" />' },
{ code: '<div role="alertdialog" />' },
{ code: '<div role="application" />' },
{ code: '<div role="article" />' },
{ code: '<div role="banner" />' },
{ code: '<div role="cell" />' },
{ code: '<div role="complementary" />' },
{ code: '<div role="contentinfo" />' },
{ code: '<div role="definition" />' },
{ code: '<div role="dialog" />' },
{ code: '<div role="directory" />' },
{ code: '<div role="document" />' },
{ code: '<div role="feed" />' },
{ code: '<div role="figure" />' },
{ code: '<div role="form" />' },
{ code: '<div role="group" />' },
{ code: '<div role="heading" />' },
{ code: '<div role="img" />' },
{ code: '<div role="list" />' },
{ code: '<div role="listitem" />' },
{ code: '<div role="log" />' },
{ code: '<div role="main" />' },
{ code: '<div role="marquee" />' },
{ code: '<div role="math" />' },
{ code: '<div role="navigation" />' },
{ code: '<div role="none" />' },
{ code: '<div role="note" />' },
{ code: '<div role="presentation" />' },
{ code: '<div role="region" />' },
{ code: '<div role="rowgroup" />' },
{ code: '<div role="search" />' },
{ code: '<div role="separator" />' },
{ code: '<div role="status" />' },
{ code: '<div role="table" />' },
{ code: '<div role="tabpanel" />' },
{ code: '<div role="term" />' },
{ code: '<div role="timer" />' },
{ code: '<div role="tooltip" />' },
// Via config
// Inputs. Ignore them because they might get a label from a wrapping label element.
{ code: '<input />' },
{ code: '<input type="button" />' },
{ code: '<input type="checkbox" />' },
{ code: '<input type="color" />' },
{ code: '<input type="date" />' },
{ code: '<input type="datetime" />' },
{ code: '<input type="email" />' },
{ code: '<input type="file" />' },
{ code: '<input type="hidden" />' },
{ code: '<input type="hidden" name="bot-field"/>' },
{ code: '<input type="hidden" name="form-name" value="Contact Form"/>' },
{ code: '<input type="image" />' },
{ code: '<input type="month" />' },
{ code: '<input type="number" />' },
{ code: '<input type="password" />' },
{ code: '<input type="radio" />' },
{ code: '<input type="range" />' },
{ code: '<input type="reset" />' },
{ code: '<input type="search" />' },
{ code: '<input type="submit" />' },
{ code: '<input type="tel" />' },
{ code: '<input type="text" />' },
{ code: '<label>Foo <input type="text" /></label>' },
{ code: '<input name={field.name} id="foo" type="text" value={field.value} disabled={isDisabled} onChange={changeText(field.onChange, field.name)} onBlur={field.onBlur} />' },
{ code: '<input type="time" />' },
{ code: '<input type="url" />' },
{ code: '<input type="week" />' },
// Marginal interactive elements. It is difficult to insist that these
// elements contain a text label.
{ code: '<audio />' },
{ code: '<canvas />' },
{ code: '<embed />' },
{ code: '<textarea />' },
{ code: '<tr />' },
{ code: '<video />' },
// Interactive roles to ignore
{ code: '<div role="grid" />' },
{ code: '<div role="listbox" />' },
{ code: '<div role="menu" />' },
{ code: '<div role="menubar" />' },
{ code: '<div role="radiogroup" />' },
{ code: '<div role="row" />' },
{ code: '<div role="tablist" />' },
{ code: '<div role="toolbar" />' },
{ code: '<div role="tree" />' },
{ code: '<div role="treegrid" />' },
];
const neverValid = [
{ code: '<button />', errors: [expectedError] },
{ code: '<button><span /></button>', errors: [expectedError] },
{ code: '<button><img /></button>', errors: [expectedError] },
{ code: '<button><span title="This is not a real label" /></button>', errors: [expectedError] },
{ code: '<button><span><span><span>Save</span></span></span></button>', options: [{ depth: 3 }], errors: [expectedError] },
{ code: '<CustomControl><span><span></span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'] }], errors: [expectedError] },
{ code: '<CustomControl></CustomControl>', errors: [expectedError], settings: { 'jsx-a11y': { components: { CustomControl: 'button' } } } },
{ code: '<a href="#" />', errors: [expectedError] },
{ code: '<area href="#" />', errors: [expectedError] },
{ code: '<menuitem />', errors: [expectedError] },
{ code: '<option />', errors: [expectedError] },
{ code: '<th />', errors: [expectedError] },
// Interactive Roles
{ code: '<div role="button" />', errors: [expectedError] },
{ code: '<div role="checkbox" />', errors: [expectedError] },
{ code: '<div role="columnheader" />', errors: [expectedError] },
{ code: '<div role="combobox" />', errors: [expectedError] },
{ code: '<div role="link" />', errors: [expectedError] },
{ code: '<div role="gridcell" />', errors: [expectedError] },
{ code: '<div role="menuitem" />', errors: [expectedError] },
{ code: '<div role="menuitemcheckbox" />', errors: [expectedError] },
{ code: '<div role="menuitemradio" />', errors: [expectedError] },
{ code: '<div role="option" />', errors: [expectedError] },
{ code: '<div role="progressbar" />', errors: [expectedError] },
{ code: '<div role="radio" />', errors: [expectedError] },
{ code: '<div role="rowheader" />', errors: [expectedError] },
{ code: '<div role="scrollbar" />', errors: [expectedError] },
{ code: '<div role="searchbox" />', errors: [expectedError] },
{ code: '<div role="slider" />', errors: [expectedError] },
{ code: '<div role="spinbutton" />', errors: [expectedError] },
{ code: '<div role="switch" />', errors: [expectedError] },
{ code: '<div role="tab" />', errors: [expectedError] },
{ code: '<div role="textbox" />', errors: [expectedError] },
];
const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
const strictOptions = (configs.strict.rules[ruleName][1] || {});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
))
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:no-config`, rule, {
valid: parsers.all([].concat(
{ code: '<input type="hidden" />' },
{ code: '<input type="text" aria-hidden="true" />' },
))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<input type="text" />', errors: [expectedError] },
))
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,85 @@
/**
* @fileoverview Enforce heading (h1, h2, etc) elements contain accessible content.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/heading-has-content';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Headings must have content and the content must be accessible by a screen reader.',
type: 'JSXOpeningElement',
};
const components = [{
components: ['Heading', 'Title'],
}];
const componentsSettings = {
'jsx-a11y': {
components: {
CustomInput: 'input',
Title: 'h1',
Heading: 'h2',
},
},
};
ruleTester.run('heading-has-content', rule, {
valid: parsers.all([].concat(
// DEFAULT ELEMENT TESTS
{ code: '<div />;' },
{ code: '<h1>Foo</h1>' },
{ code: '<h2>Foo</h2>' },
{ code: '<h3>Foo</h3>' },
{ code: '<h4>Foo</h4>' },
{ code: '<h5>Foo</h5>' },
{ code: '<h6>Foo</h6>' },
{ code: '<h6>123</h6>' },
{ code: '<h1><Bar /></h1>' },
{ code: '<h1>{foo}</h1>' },
{ code: '<h1>{foo.bar}</h1>' },
{ code: '<h1 dangerouslySetInnerHTML={{ __html: "foo" }} />' },
{ code: '<h1 children={children} />' },
// CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
{ code: '<Heading>Foo</Heading>', options: components },
{ code: '<Title>Foo</Title>', options: components },
{ code: '<Heading><Bar /></Heading>', options: components },
{ code: '<Heading>{foo}</Heading>', options: components },
{ code: '<Heading>{foo.bar}</Heading>', options: components },
{ code: '<Heading dangerouslySetInnerHTML={{ __html: "foo" }} />', options: components },
{ code: '<Heading children={children} />', options: components },
{ code: '<h1 aria-hidden />' },
// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
{ code: '<Heading>Foo</Heading>', settings: componentsSettings },
{ code: '<h1><CustomInput type="hidden" /></h1>' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// DEFAULT ELEMENT TESTS
{ code: '<h1 />', errors: [expectedError] },
{ code: '<h1><Bar aria-hidden /></h1>', errors: [expectedError] },
{ code: '<h1>{undefined}</h1>', errors: [expectedError] },
{ code: '<h1><input type="hidden" /></h1>', errors: [expectedError] },
// CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
{ code: '<Heading />', errors: [expectedError], options: components },
{ code: '<Heading><Bar aria-hidden /></Heading>', errors: [expectedError], options: components },
{ code: '<Heading>{undefined}</Heading>', errors: [expectedError], options: components },
// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
{ code: '<Heading />', errors: [expectedError], settings: componentsSettings },
{ code: '<h1><CustomInput type="hidden" /></h1>', errors: [expectedError], settings: componentsSettings },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,42 @@
/**
* @fileoverview Enforce html element has lang prop.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/html-has-lang';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: '<html> elements must have the lang prop.',
type: 'JSXOpeningElement',
};
ruleTester.run('html-has-lang', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<html lang="en" />' },
{ code: '<html lang="en-US" />' },
{ code: '<html lang={foo} />' },
{ code: '<html lang />' },
{ code: '<HTML />' },
{ code: '<HTMLTop lang="en" />', errors: [expectedError], settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<html />', errors: [expectedError] },
{ code: '<html {...props} />', errors: [expectedError] },
{ code: '<html lang={undefined} />', errors: [expectedError] },
{ code: '<HTMLTop />', errors: [expectedError], settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,55 @@
/**
* @fileoverview Enforce iframe elements have a title attribute.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/iframe-has-title';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: '<iframe> elements must have a unique title property.',
type: 'JSXOpeningElement',
};
const componentsSettings = {
'jsx-a11y': {
components: {
FooComponent: 'iframe',
},
},
};
ruleTester.run('html-has-lang', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<iframe title="Unique title" />' },
{ code: '<iframe title={foo} />' },
{ code: '<FooComponent />' },
{ code: '<FooComponent title="Unique title" />', settings: componentsSettings },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<iframe />', errors: [expectedError] },
{ code: '<iframe {...props} />', errors: [expectedError] },
{ code: '<iframe title={undefined} />', errors: [expectedError] },
{ code: '<iframe title="" />', errors: [expectedError] },
{ code: '<iframe title={false} />', errors: [expectedError] },
{ code: '<iframe title={true} />', errors: [expectedError] },
{ code: "<iframe title={''} />", errors: [expectedError] },
{ code: '<iframe title={``} />', errors: [expectedError] },
{ code: '<iframe title={""} />', errors: [expectedError] },
{ code: '<iframe title={42} />', errors: [expectedError] },
{ code: '<FooComponent />', errors: [expectedError], settings: componentsSettings },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,137 @@
/**
* @fileoverview Enforce img alt attribute does not have the word image, picture, or photo.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import semver from 'semver';
import { version as eslintVersion } from 'eslint/package.json';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/img-redundant-alt';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const array = [{
components: ['Image'],
words: ['Word1', 'Word2'],
}];
const componentsSettings = {
'jsx-a11y': {
components: {
Image: 'img',
},
},
};
const ruleTester = new RuleTester();
const expectedError = {
message: 'Redundant alt attribute. Screen-readers already announce `img` tags as an image. You dont need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt prop.',
type: 'JSXOpeningElement',
};
ruleTester.run('img-redundant-alt', rule, {
valid: parsers.all([].concat(
{ code: '<img alt="foo" />;' },
{ code: '<img alt="picture of me taking a photo of an image" aria-hidden />' },
{ code: '<img aria-hidden alt="photo of image" />' },
{ code: '<img ALt="foo" />;' },
{ code: '<img {...this.props} alt="foo" />' },
{ code: '<img {...this.props} alt={"foo"} />' },
{ code: '<img {...this.props} alt={alt} />' },
{ code: '<a />' },
{ code: '<img />' },
{ code: '<IMG />' },
{ code: '<img alt={undefined} />' },
{ code: '<img alt={`this should pass for ${now}`} />' },
{ code: '<img alt={`this should pass for ${photo}`} />' },
{ code: '<img alt={`this should pass for ${image}`} />' },
{ code: '<img alt={`this should pass for ${picture}`} />' },
{ code: '<img alt={`${photo}`} />' },
{ code: '<img alt={`${image}`} />' },
{ code: '<img alt={`${picture}`} />' },
{ code: '<img alt={"undefined"} />' },
{ code: '<img alt={() => {}} />' },
{ code: '<img alt={function(e){}} />' },
{ code: '<img aria-hidden={false} alt="Doing cool things." />' },
{ code: '<UX.Layout>test</UX.Layout>' },
{ code: '<img alt />' },
{ code: '<img alt={imageAlt} />' },
{ code: '<img alt={imageAlt.name} />' },
semver.satisfies(eslintVersion, '>= 6') ? [
{ code: '<img alt={imageAlt?.name} />', languageOptions: { ecmaVersion: 2020 } },
{ code: '<img alt="Doing cool things" aria-hidden={foo?.bar}/>', languageOptions: { ecmaVersion: 2020 } },
] : [],
{ code: '<img alt="Photography" />;' },
{ code: '<img alt="ImageMagick" />;' },
{ code: '<Image alt="Photo of a friend" />' },
{ code: '<Image alt="Foo" />', settings: componentsSettings },
{ code: '<img alt="画像" />', options: [{ words: ['イメージ'] }] },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<img alt="Photo of friend." />;', errors: [expectedError] },
{ code: '<img alt="Picture of friend." />;', errors: [expectedError] },
{ code: '<img alt="Image of friend." />;', errors: [expectedError] },
{ code: '<img alt="PhOtO of friend." />;', errors: [expectedError] },
{ code: '<img alt={"photo"} />;', errors: [expectedError] },
{ code: '<img alt="piCTUre of friend." />;', errors: [expectedError] },
{ code: '<img alt="imAGE of friend." />;', errors: [expectedError] },
{
code: '<img alt="photo of cool person" aria-hidden={false} />',
errors: [expectedError],
},
{
code: '<img alt="picture of cool person" aria-hidden={false} />',
errors: [expectedError],
},
{
code: '<img alt="image of cool person" aria-hidden={false} />',
errors: [expectedError],
},
{ code: '<img alt="photo" {...this.props} />', errors: [expectedError] },
{ code: '<img alt="image" {...this.props} />', errors: [expectedError] },
{ code: '<img alt="picture" {...this.props} />', errors: [expectedError] },
{
code: '<img alt={`picture doing ${things}`} {...this.props} />',
errors: [expectedError],
},
{
code: '<img alt={`photo doing ${things}`} {...this.props} />',
errors: [expectedError],
},
{
code: '<img alt={`image doing ${things}`} {...this.props} />',
errors: [expectedError],
},
{
code: '<img alt={`picture doing ${picture}`} {...this.props} />',
errors: [expectedError],
},
{
code: '<img alt={`photo doing ${photo}`} {...this.props} />',
errors: [expectedError],
},
{
code: '<img alt={`image doing ${image}`} {...this.props} />',
errors: [expectedError],
},
{ code: '<Image alt="Photo of a friend" />', errors: [expectedError], settings: componentsSettings },
// TESTS FOR ARRAY OPTION TESTS
{ code: '<img alt="Word1" />;', options: array, errors: [expectedError] },
{ code: '<img alt="Word2" />;', options: array, errors: [expectedError] },
{ code: '<Image alt="Word1" />;', options: array, errors: [expectedError] },
{ code: '<Image alt="Word2" />;', options: array, errors: [expectedError] },
{ code: '<img alt="イメージ" />', options: [{ words: ['イメージ'] }], errors: [expectedError] },
{ code: '<img alt="イメージです" />', options: [{ words: ['イメージ'] }], errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,267 @@
/**
* @fileoverview Enforce that elements with onClick handlers must be focusable.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import includes from 'array-includes';
import { RuleTester } from 'eslint';
import {
eventHandlers,
eventHandlersByType,
} from 'jsx-ast-utils';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/interactive-supports-focus';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
function template(strings, ...keys) {
return (...values) => keys.reduce(
(acc, k, i) => acc + (values[k] || '') + strings[i + 1],
strings[0],
);
}
const ruleName = 'interactive-supports-focus';
const type = 'JSXOpeningElement';
const codeTemplate = template`<${0} role="${1}" ${2}={() => void 0} />`;
const tabindexTemplate = template`<${0} role="${1}" ${2}={() => void 0} tabIndex="0" />`;
const tabbableTemplate = template`Elements with the '${0}' interactive role must be tabbable.`;
const focusableTemplate = template`Elements with the '${0}' interactive role must be focusable.`;
const componentsSettings = {
'jsx-a11y': {
components: {
Div: 'div',
},
},
};
const buttonError = { message: tabbableTemplate('button'), type };
const recommendedOptions = configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {};
const strictOptions = configs.strict.rules[`jsx-a11y/${ruleName}`][1] || {};
const alwaysValid = [
{ code: '<div />' },
{ code: '<div aria-hidden onClick={() => void 0} />' },
{ code: '<div aria-hidden={true == true} onClick={() => void 0} />' },
{ code: '<div aria-hidden={true === true} onClick={() => void 0} />' },
{ code: '<div aria-hidden={hidden !== false} onClick={() => void 0} />' },
{ code: '<div aria-hidden={hidden != false} onClick={() => void 0} />' },
{ code: '<div aria-hidden={1 < 2} onClick={() => void 0} />' },
{ code: '<div aria-hidden={1 <= 2} onClick={() => void 0} />' },
{ code: '<div aria-hidden={2 > 1} onClick={() => void 0} />' },
{ code: '<div aria-hidden={2 >= 1} onClick={() => void 0} />' },
{ code: '<div onClick={() => void 0} />;' },
{ code: '<div onClick={() => void 0} tabIndex={undefined} />;' },
{ code: '<div onClick={() => void 0} tabIndex="bad" />;' },
{ code: '<div onClick={() => void 0} role={undefined} />;' },
{ code: '<div role="section" onClick={() => void 0} />' },
{ code: '<div onClick={() => void 0} aria-hidden={false} />;' },
{ code: '<div onClick={() => void 0} {...props} />;' },
{ code: '<input type="text" onClick={() => void 0} />' },
{ code: '<input type="hidden" onClick={() => void 0} tabIndex="-1" />' },
{ code: '<input type="hidden" onClick={() => void 0} tabIndex={-1} />' },
{ code: '<input onClick={() => void 0} />' },
{ code: '<input onClick={() => void 0} role="combobox" />' },
{ code: '<button onClick={() => void 0} className="foo" />' },
{ code: '<option onClick={() => void 0} className="foo" />' },
{ code: '<select onClick={() => void 0} className="foo" />' },
{ code: '<area href="#" onClick={() => void 0} className="foo" />' },
{ code: '<area onClick={() => void 0} className="foo" />' },
{ code: '<summary onClick={() => void 0} />' },
{ code: '<textarea onClick={() => void 0} className="foo" />' },
{ code: '<a onClick="showNextPage();">Next page</a>' },
{ code: '<a onClick="showNextPage();" tabIndex={undefined}>Next page</a>' },
{ code: '<a onClick="showNextPage();" tabIndex="bad">Next page</a>' },
{ code: '<a onClick={() => void 0} />' },
{ code: '<a tabIndex="0" onClick={() => void 0} />' },
{ code: '<a tabIndex={dynamicTabIndex} onClick={() => void 0} />' },
{ code: '<a tabIndex={0} onClick={() => void 0} />' },
{ code: '<a role="button" href="#" onClick={() => void 0} />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex={0} />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" role="button" />' },
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<input onClick={() => void 0} type="hidden" />;' },
{ code: '<span onClick="submitForm();">Submit</span>' },
{ code: '<span onClick="submitForm();" tabIndex={undefined}>Submit</span>' },
{ code: '<span onClick="submitForm();" tabIndex="bad">Submit</span>' },
{ code: '<span onClick="doSomething();" tabIndex="0">Click me!</span>' },
{ code: '<span onClick="doSomething();" tabIndex={0}>Click me!</span>' },
{ code: '<span onClick="doSomething();" tabIndex="-1">Click me too!</span>' },
{
code: '<a href="javascript:void(0);" onClick="doSomething();">Click ALL the things!</a>',
},
{ code: '<section onClick={() => void 0} />;' },
{ code: '<main onClick={() => void 0} />;' },
{ code: '<article onClick={() => void 0} />;' },
{ code: '<header onClick={() => void 0} />;' },
{ code: '<footer onClick={() => void 0} />;' },
{ code: '<div role="button" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="checkbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="link" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitem" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitemcheckbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitemradio" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="option" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="radio" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="spinbutton" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="switch" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="tablist" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="tab" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="textbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="textbox" aria-disabled="true" onClick={() => void 0} />' },
{ code: '<Foo.Bar onClick={() => void 0} aria-hidden={false} />;' },
{ code: '<Input onClick={() => void 0} type="hidden" />;' },
{ code: '<Div onClick={() => void 0} role="button" tabIndex="0" />', settings: componentsSettings },
];
const neverValid = [
{ code: '<Div onClick={() => void 0} role="button" />', errors: [buttonError], settings: componentsSettings },
];
const interactiveRoles = [
'button',
'checkbox',
'link',
'gridcell',
'menuitem',
'menuitemcheckbox',
'menuitemradio',
'option',
'radio',
'searchbox',
'slider',
'spinbutton',
'switch',
'tab',
'textbox',
'treeitem',
];
const recommendedRoles = [
'button',
'checkbox',
'link',
'searchbox',
'spinbutton',
'switch',
'textbox',
];
const strictRoles = [
'button',
'checkbox',
'link',
'progressbar',
'searchbox',
'slider',
'spinbutton',
'switch',
'textbox',
];
const staticElements = [
'div',
];
const triggeringHandlers = [
...eventHandlersByType.mouse,
...eventHandlersByType.keyboard,
];
const passReducer = (roles, handlers, messageTemplate) => (
staticElements.reduce((elementAcc, element) => (
elementAcc.concat(roles.reduce((roleAcc, role) => (
roleAcc.concat(handlers.map((handler) => ({
code: messageTemplate(element, role, handler),
})))
), []))
), [])
);
const failReducer = (roles, handlers, messageTemplate) => (
staticElements.reduce((elementAcc, element) => (
elementAcc.concat(roles.reduce((roleAcc, role) => (
roleAcc.concat(handlers.map((handler) => ({
code: codeTemplate(element, role, handler),
errors: [{
type,
message: messageTemplate(role),
}],
})))
), []))
), [])
);
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
...passReducer(
interactiveRoles,
eventHandlers.filter((handler) => !includes(triggeringHandlers, handler)),
codeTemplate,
),
...passReducer(
interactiveRoles.filter((role) => !includes(recommendedRoles, role)),
eventHandlers.filter((handler) => includes(triggeringHandlers, handler)),
tabindexTemplate,
),
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
...failReducer(recommendedRoles, triggeringHandlers, tabbableTemplate),
...failReducer(
interactiveRoles.filter((role) => !includes(recommendedRoles, role)),
triggeringHandlers,
focusableTemplate,
),
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
...passReducer(
interactiveRoles,
eventHandlers.filter((handler) => !includes(triggeringHandlers, handler)),
codeTemplate,
),
...passReducer(
interactiveRoles.filter((role) => !includes(strictRoles, role)),
eventHandlers.filter((handler) => includes(triggeringHandlers, handler)),
tabindexTemplate,
),
))
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
...failReducer(strictRoles, triggeringHandlers, tabbableTemplate),
...failReducer(
interactiveRoles.filter((role) => !includes(strictRoles, role)),
triggeringHandlers,
focusableTemplate,
),
))
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,241 @@
/**
* @fileoverview Enforce label tags have an associated control.
* @author Jesse Beach
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/label-has-associated-control';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const ruleName = 'label-has-associated-control';
const expectedError = {
message: 'A form label must be associated with a control.',
type: 'JSXOpeningElement',
};
const expectedErrorNoLabel = {
message: 'A form label must have accessible text.',
type: 'JSXOpeningElement',
};
const componentsSettings = {
'jsx-a11y': {
components: {
CustomInput: 'input',
CustomLabel: 'label',
},
},
};
const attributesSettings = {
'jsx-a11y': {
attributes: {
for: ['htmlFor', 'for'],
},
},
};
const htmlForValid = [
{ code: '<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }] },
{ code: '<label htmlFor="js_id" aria-label="A label" />' },
{ code: '<label htmlFor="js_id" aria-labelledby="A label" />' },
{ code: '<div><label htmlFor="js_id">A label</label><input id="js_id" /></div>' },
{ code: '<label for="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }], settings: attributesSettings },
{ code: '<label for="js_id" aria-label="A label" />', settings: attributesSettings },
{ code: '<label for="js_id" aria-labelledby="A label" />', settings: attributesSettings },
{ code: '<div><label for="js_id">A label</label><input id="js_id" /></div>', settings: attributesSettings },
// Custom label component.
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', settings: componentsSettings },
// Custom label attributes.
{ code: '<label htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'] }] },
// Glob support for controlComponents option.
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ controlComponents: ['Custom*'] }] },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ controlComponents: ['*Label'] }] },
// Rule does not error if presence of accessible label cannot be determined
{ code: '<div><label htmlFor="js_id"><CustomText /></label><input id="js_id" /></div>' },
];
const nestingValid = [
{ code: '<label>A label<input /></label>' },
{ code: '<label>A label<textarea /></label>' },
{ code: '<label><img alt="A label" /><input /></label>' },
{ code: '<label><img aria-label="A label" /><input /></label>' },
{ code: '<label><span>A label<input /></span></label>' },
{ code: '<label><span><span>A label<input /></span></span></label>', options: [{ depth: 3 }] },
{ code: '<label><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }] },
{ code: '<label><span><span><span><span>A label</span><input /></span></span></span></label>', options: [{ depth: 5 }] },
{ code: '<label><span><span><span><span aria-label="A label" /><input /></span></span></span></label>', options: [{ depth: 5 }] },
{ code: '<label><span><span><span><input aria-label="A label" /></span></span></span></label>', options: [{ depth: 5 }] },
// Other controls
{ code: '<label>foo<meter /></label>' },
{ code: '<label>foo<output /></label>' },
{ code: '<label>foo<progress /></label>' },
{ code: '<label>foo<textarea /></label>' },
// Custom controlComponents.
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }] },
{ code: '<label><span>A label<CustomInput /></span></label>', settings: componentsSettings },
{ code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel><span label="A label"><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }] },
// Glob support for controlComponents option.
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['Custom*'] }] },
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['*Input'] }] },
// Rule does not error if presence of accessible label cannot be determined
{ code: '<label><CustomText /><input /></label>' },
];
const bothValid = [
{ code: '<label htmlFor="js_id"><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }] },
{ code: '<label htmlFor="js_id" aria-label="A label"><input /></label>' },
{ code: '<label htmlFor="js_id" aria-labelledby="A label"><input /></label>' },
{ code: '<label htmlFor="js_id" aria-labelledby="A label"><textarea /></label>' },
// Custom label component.
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label"><input /></CustomLabel>', options: [{ labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" label="A label"><input /></CustomLabel>', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label"><input /></CustomLabel>', settings: componentsSettings },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label"><CustomInput /></CustomLabel>', settings: componentsSettings },
// Custom label attributes.
{ code: '<label htmlFor="js_id" label="A label"><input /></label>', options: [{ labelAttributes: ['label'] }] },
{ code: '<label htmlFor="selectInput">Some text<select id="selectInput" /></label>' },
];
const alwaysValid = [
{ code: '<div />' },
{ code: '<CustomElement />' },
{ code: '<input type="hidden" />' },
];
const htmlForInvalid = [
{ code: '<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }], errors: [expectedError] },
{ code: '<label htmlFor="js_id" aria-label="A label" />', errors: [expectedError] },
{ code: '<label htmlFor="js_id" aria-labelledby="A label" />', errors: [expectedError] },
// Custom label component.
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<CustomLabel htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', settings: componentsSettings, errors: [expectedError] },
// Custom label attributes.
{ code: '<label htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'] }], errors: [expectedError] },
];
const nestingInvalid = [
{ code: '<label>A label<input /></label>', errors: [expectedError] },
{ code: '<label>A label<textarea /></label>', errors: [expectedError] },
{ code: '<label><img alt="A label" /><input /></label>', errors: [expectedError] },
{ code: '<label><img aria-label="A label" /><input /></label>', errors: [expectedError] },
{ code: '<label><span>A label<input /></span></label>', errors: [expectedError] },
{ code: '<label><span><span>A label<input /></span></span></label>', options: [{ depth: 3 }], errors: [expectedError] },
{ code: '<label><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }], errors: [expectedError] },
{ code: '<label><span><span><span><span>A label</span><input /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
{ code: '<label><span><span><span><span aria-label="A label" /><input /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
{ code: '<label><span><span><span><input aria-label="A label" /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
// Custom controlComponents.
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }], errors: [expectedError] },
{ code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<CustomLabel><span label="A label"><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }], errors: [expectedError] },
{ code: '<label><span>A label<CustomInput /></span></label>', settings: componentsSettings, errors: [expectedError] },
{ code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', settings: componentsSettings, errors: [expectedError] },
];
const neverValid = [
{ code: '<label htmlFor="js_id" />', errors: [expectedErrorNoLabel] },
{ code: '<label htmlFor="js_id"><input /></label>', errors: [expectedErrorNoLabel] },
{ code: '<label htmlFor="js_id"><textarea /></label>', errors: [expectedErrorNoLabel] },
{ code: '<label></label>', errors: [expectedErrorNoLabel] },
{ code: '<label>A label</label>', errors: [expectedError] },
{ code: '<div><label /><input /></div>', errors: [expectedErrorNoLabel] },
{ code: '<div><label>A label</label><input /></div>', errors: [expectedError] },
// Custom label component.
{ code: '<CustomLabel aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<CustomLabel label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<CustomLabel aria-label="A label" />', settings: componentsSettings, errors: [expectedError] },
// Custom label attributes.
{ code: '<label label="A label" />', options: [{ labelAttributes: ['label'] }], errors: [expectedError] },
// Custom controlComponents.
{ code: '<label><span><CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }], errors: [expectedErrorNoLabel] },
{ code: '<CustomLabel><span><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }], errors: [expectedErrorNoLabel] },
{ code: '<CustomLabel><span><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }], errors: [expectedErrorNoLabel] },
{ code: '<label><span><CustomInput /></span></label>', settings: componentsSettings, errors: [expectedErrorNoLabel] },
{ code: '<CustomLabel><span><CustomInput /></span></CustomLabel>', settings: componentsSettings, errors: [expectedErrorNoLabel] },
];
// htmlFor valid
ruleTester.run(ruleName, rule, {
valid: parsers.all([].concat(
...alwaysValid,
...htmlForValid,
))
.map(ruleOptionsMapperFactory({
assert: 'htmlFor',
}))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
...nestingInvalid,
))
.map(ruleOptionsMapperFactory({
assert: 'htmlFor',
}))
.map(parserOptionsMapper),
});
// nesting valid
ruleTester.run(ruleName, rule, {
valid: parsers.all([].concat(
...alwaysValid,
...nestingValid,
))
.map(ruleOptionsMapperFactory({
assert: 'nesting',
}))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
...htmlForInvalid,
))
.map(ruleOptionsMapperFactory({
assert: 'nesting',
}))
.map(parserOptionsMapper),
});
// either valid
ruleTester.run(ruleName, rule, {
valid: parsers.all([].concat(
...alwaysValid,
...htmlForValid,
...nestingValid,
))
.map(ruleOptionsMapperFactory({
assert: 'either',
}))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
)).map(parserOptionsMapper),
});
// both valid
ruleTester.run(ruleName, rule, {
valid: parsers.all([].concat(
...alwaysValid,
...bothValid,
))
.map(ruleOptionsMapperFactory({
assert: 'both',
}))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,235 @@
/**
* @fileoverview Enforce label tags have htmlFor attribute.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import assign from 'object.assign';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/label-has-for';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedNestingError = {
message: 'Form label must have the following type of associated control: nesting',
type: 'JSXOpeningElement',
};
const expectedSomeError = {
message: 'Form label must have ANY of the following types of associated control: nesting, id',
type: 'JSXOpeningElement',
};
const expectedEveryError = {
message: 'Form label must have ALL of the following types of associated control: nesting, id',
type: 'JSXOpeningElement',
};
const optionsComponents = [{
components: ['Label', 'Descriptor'],
}];
const optionsRequiredNesting = [{
required: 'nesting',
}];
const optionsRequiredSome = [{
required: { some: ['nesting', 'id'] },
}];
const optionsRequiredEvery = [{
required: { every: ['nesting', 'id'] },
}];
const optionsChildrenAllowed = [{
allowChildren: true,
}];
const attributesSettings = {
'jsx-a11y': {
attributes: {
for: ['htmlFor', 'for'],
},
},
};
ruleTester.run('label-has-for', rule, {
valid: parsers.all([].concat(
// DEFAULT ELEMENT 'label' TESTS
{ code: '<div />' },
{ code: '<label htmlFor="foo"><input /></label>' },
{ code: '<label htmlFor="foo"><textarea /></label>' },
{ code: '<label for="foo"><input /></label>', settings: attributesSettings },
{ code: '<label for="foo"><textarea /></label>', settings: attributesSettings },
{ code: '<Label />' }, // lower-case convention refers to real HTML elements.
{ code: '<Label htmlFor="foo" />' },
{ code: '<Label for="foo" />', settings: attributesSettings },
{ code: '<Descriptor />' },
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>' },
{ code: '<Descriptor for="foo">Test!</Descriptor>', settings: attributesSettings },
{ code: '<UX.Layout>test</UX.Layout>' },
// CUSTOM ELEMENT ARRAY OPTION TESTS
{ code: '<Label htmlFor="foo" />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Label htmlFor={"foo"} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Label htmlFor={foo} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Label htmlFor={`${id}`} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<div />', options: optionsComponents },
{ code: '<Label htmlFor="something"><input /></Label>', options: optionsComponents },
{ code: '<Label htmlFor="foo">Test!</Label>', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor="foo" />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor={"foo"} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor={foo} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor={`${id}`} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<label htmlFor="foo" />', options: optionsRequiredSome },
{ code: '<label htmlFor={"foo"} />', options: optionsRequiredSome },
{ code: '<label htmlFor={foo} />', options: optionsRequiredSome },
{ code: '<label htmlFor={`${id}`} />', options: optionsRequiredSome },
{ code: '<label htmlFor="foo">Test!</label>', options: optionsRequiredSome },
{ code: '<label><input /></label>', options: optionsRequiredSome },
{ code: '<label><input /></label>', options: optionsRequiredNesting },
{ code: '<label htmlFor="input"><input /></label>', options: optionsRequiredEvery },
{ code: '<label><input /></label>', options: optionsChildrenAllowed },
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>', options: [assign({}, optionsComponents, optionsChildrenAllowed)] },
{ code: '<label>Test!</label>', options: optionsChildrenAllowed },
{ code: '<label htmlFor="foo">Test!</label>', options: optionsChildrenAllowed },
{ code: '<label>{children}</label>', options: optionsChildrenAllowed },
{ code: '<label htmlFor="children">{children}</label>', options: optionsChildrenAllowed },
{ code: '<label htmlFor={id}>{ labelText }<div><input id={id} type="checkbox" name={id} value={value} /></div></label>', options: optionsRequiredEvery },
{ code: '<label htmlFor={id}>{ labelText }<div><div><div><div><input id={id} type="checkbox" name={id} value={value} /></div></div></div></div></label>', options: optionsRequiredEvery },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// DEFAULT ELEMENT 'label' TESTS
{ code: '<label id="foo" />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={undefined} />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={`${undefined}`} />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label>First Name</label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label {...props}>Foo</label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label><input /></label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label><textarea /></label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label>{children}</label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor="foo" />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={"foo"} />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={foo} />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={`${id}`} />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor="foo">Test!</label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={id}>{ labelText }<div><div><div><div><div id={id} type="checkbox" name={id} value={value} /></div></div></div></div></label>', errors: [expectedEveryError], options: optionsRequiredEvery },
//
// // CUSTOM ELEMENT ARRAY OPTION TESTS
{
code: '<Label></Label>',
errors: [expectedEveryError],
options: optionsComponents,
},
{
code: '<Label htmlFor="foo" />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Label htmlFor={"foo"} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Label htmlFor={foo} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Label htmlFor={`${id}`} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Label htmlFor="foo">Test!</Label>',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Descriptor htmlFor="foo" />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Descriptor htmlFor={"foo"} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Descriptor htmlFor={foo} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Descriptor htmlFor={`${id}`} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Descriptor htmlFor="foo">Test!</Descriptor>',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{ code: '<Label id="foo" />', errors: [expectedEveryError], options: optionsComponents },
{
code: '<Label htmlFor={undefined} />',
errors: [expectedEveryError],
options: optionsComponents,
},
{
code: '<Label htmlFor={`${undefined}`} />',
errors: [expectedEveryError],
options: optionsComponents,
},
{ code: '<Label>First Name</Label>', errors: [expectedEveryError], options: optionsComponents },
{
code: '<Label {...props}>Foo</Label>',
errors: [expectedEveryError],
options: optionsComponents,
},
{ code: '<Descriptor id="foo" />', errors: [expectedEveryError], options: optionsComponents },
{
code: '<Descriptor htmlFor={undefined} />',
errors: [expectedEveryError],
options: optionsComponents,
},
{
code: '<Descriptor htmlFor={`${undefined}`} />',
errors: [expectedEveryError],
options: optionsComponents,
},
{
code: '<Descriptor>First Name</Descriptor>',
errors: [expectedEveryError],
options: optionsComponents,
},
{
code: '<Descriptor {...props}>Foo</Descriptor>',
errors: [expectedEveryError],
options: optionsComponents,
},
{ code: '<label>{children}</label>', errors: [expectedEveryError], options: optionsComponents },
{ code: '<label htmlFor="foo" />', errors: [expectedNestingError], options: optionsRequiredNesting },
{ code: '<label>First Name</label>', errors: [expectedNestingError], options: optionsRequiredNesting },
{ code: '<label>First Name</label>', errors: [expectedSomeError], options: optionsRequiredSome },
{ code: '<label>{children}</label>', errors: [expectedSomeError], options: optionsRequiredSome },
{ code: '<label>{children}</label>', errors: [expectedNestingError], options: optionsRequiredNesting },
{
code: '<form><input type="text" id="howmuch" value="1" /><label htmlFor="howmuch">How much ?</label></form>',
errors: [expectedEveryError],
options: optionsRequiredEvery,
},
{
code: '<form><input type="text" id="howmuch" value="1" /><label htmlFor="howmuch">How much ?<span /></label></form>',
errors: [expectedEveryError],
options: optionsRequiredEvery,
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,59 @@
/**
* @fileoverview Enforce lang attribute has a valid value.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/lang';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'lang attribute must have a valid value.',
type: 'JSXAttribute',
};
const componentsSettings = {
'jsx-a11y': {
polymorphicPropName: 'as',
components: {
Foo: 'html',
},
},
};
ruleTester.run('lang', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<div foo="bar" />;' },
{ code: '<div lang="foo" />;' },
{ code: '<html lang="en" />' },
{ code: '<html lang="en-US" />' },
{ code: '<html lang="zh-Hans" />' },
{ code: '<html lang="zh-Hant-HK" />' },
{ code: '<html lang="zh-yue-Hant" />' },
{ code: '<html lang="ja-Latn" />' },
{ code: '<html lang={foo} />' },
{ code: '<HTML lang="foo" />' },
{ code: '<Foo lang={undefined} />' },
{ code: '<Foo lang="en" />', settings: componentsSettings },
{ code: '<Box as="html" lang="en" />', settings: componentsSettings },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<html lang="foo" />', errors: [expectedError] },
{ code: '<html lang="zz-LL" />', errors: [expectedError] },
{ code: '<html lang={undefined} />', errors: [expectedError] },
{ code: '<Foo lang={undefined} />', settings: componentsSettings, errors: [expectedError] },
{ code: '<Box as="html" lang="foo" />', settings: componentsSettings, errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,220 @@
/**
* @fileoverview <audio> and <video> elements must have a <track> for captions.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/media-has-caption';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Media elements such as <audio> and <video> must have a <track> for captions.',
type: 'JSXOpeningElement',
};
const customSchema = [
{
audio: ['Audio'],
video: ['Video'],
track: ['Track'],
},
];
const componentsSettings = {
'jsx-a11y': {
polymorphicPropName: 'as',
components: {
Audio: 'audio',
Video: 'video',
Track: 'track',
},
},
};
ruleTester.run('media-has-caption', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<MyDiv />;' },
{ code: '<audio><track kind="captions" /></audio>' },
{ code: '<audio><track kind="Captions" /></audio>' },
{
code: '<audio><track kind="Captions" /><track kind="subtitles" /></audio>',
},
{ code: '<video><track kind="captions" /></video>' },
{ code: '<video><track kind="Captions" /></video>' },
{
code: '<video><track kind="Captions" /><track kind="subtitles" /></video>',
},
{
code: '<audio muted={true}></audio>',
},
{
code: '<video muted={true}></video>',
},
{
code: '<video muted></video>',
},
{
code: '<Audio><track kind="captions" /></Audio>',
options: customSchema,
},
{
code: '<audio><Track kind="captions" /></audio>',
options: customSchema,
},
{
code: '<Video><track kind="captions" /></Video>',
options: customSchema,
},
{
code: '<video><Track kind="captions" /></video>',
options: customSchema,
},
{
code: '<Audio><Track kind="captions" /></Audio>',
options: customSchema,
},
{
code: '<Video><Track kind="captions" /></Video>',
options: customSchema,
},
{
code: '<Video muted></Video>',
options: customSchema,
},
{
code: '<Video muted={true}></Video>',
options: customSchema,
},
{
code: '<Audio muted></Audio>',
options: customSchema,
},
{
code: '<Audio muted={true}></Audio>',
options: customSchema,
},
{
code: '<Audio><track kind="captions" /></Audio>',
settings: componentsSettings,
},
{
code: '<audio><Track kind="captions" /></audio>',
settings: componentsSettings,
},
{
code: '<Video><track kind="captions" /></Video>',
settings: componentsSettings,
},
{
code: '<video><Track kind="captions" /></video>',
settings: componentsSettings,
},
{
code: '<Audio><Track kind="captions" /></Audio>',
settings: componentsSettings,
},
{
code: '<Video><Track kind="captions" /></Video>',
settings: componentsSettings,
},
{
code: '<Video muted></Video>',
settings: componentsSettings,
},
{
code: '<Video muted={true}></Video>',
settings: componentsSettings,
},
{
code: '<Audio muted></Audio>',
settings: componentsSettings,
},
{
code: '<Audio muted={true}></Audio>',
settings: componentsSettings,
},
{
code: '<Box as="audio" muted={true}></Box>',
settings: componentsSettings,
},
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<audio><track /></audio>', errors: [expectedError] },
{
code: '<audio><track kind="subtitles" /></audio>',
errors: [expectedError],
},
{ code: '<audio />', errors: [expectedError] },
{ code: '<video><track /></video>', errors: [expectedError] },
{
code: '<video><track kind="subtitles" /></video>',
errors: [expectedError],
},
{
code: '<Audio muted={false}></Audio>',
options: customSchema,
errors: [expectedError],
},
{
code: '<Video muted={false}></Video>',
options: customSchema,
errors: [expectedError],
},
{
code: '<Audio muted={false}></Audio>',
settings: componentsSettings,
errors: [expectedError],
},
{
code: '<Video muted={false}></Video>',
settings: componentsSettings,
errors: [expectedError],
},
{ code: '<video />', errors: [expectedError] },
{ code: '<audio>Foo</audio>', errors: [expectedError] },
{ code: '<video>Foo</video>', errors: [expectedError] },
{ code: '<Audio />', options: customSchema, errors: [expectedError] },
{ code: '<Video />', options: customSchema, errors: [expectedError] },
{ code: '<Audio />', settings: componentsSettings, errors: [expectedError] },
{ code: '<Video />', settings: componentsSettings, errors: [expectedError] },
{ code: '<audio><Track /></audio>', options: customSchema, errors: [expectedError] },
{ code: '<video><Track /></video>', options: customSchema, errors: [expectedError] },
{
code: '<Audio><Track kind="subtitles" /></Audio>',
options: customSchema,
errors: [expectedError],
},
{
code: '<Video><Track kind="subtitles" /></Video>',
options: customSchema,
errors: [expectedError],
},
{
code: '<Audio><Track kind="subtitles" /></Audio>',
settings: componentsSettings,
errors: [expectedError],
},
{
code: '<Video><Track kind="subtitles" /></Video>',
settings: componentsSettings,
errors: [expectedError],
},
{
code: '<Box as="audio"><Track kind="subtitles" /></Box>',
settings: componentsSettings,
errors: [expectedError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,154 @@
/**
* @fileoverview Enforce onmouseover/onmouseout are accompanied
* by onfocus/onblur.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/mouse-events-have-key-events';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const mouseOverError = {
message: 'onMouseOver must be accompanied by onFocus for accessibility.',
type: 'JSXAttribute',
};
const pointerEnterError = {
message: 'onPointerEnter must be accompanied by onFocus for accessibility.',
type: 'JSXAttribute',
};
const mouseOutError = {
message: 'onMouseOut must be accompanied by onBlur for accessibility.',
type: 'JSXAttribute',
};
const pointerLeaveError = {
message: 'onPointerLeave must be accompanied by onBlur for accessibility.',
type: 'JSXAttribute',
};
ruleTester.run('mouse-events-have-key-events', rule, {
valid: parsers.all([].concat(
{ code: '<div onMouseOver={() => void 0} onFocus={() => void 0} />;' },
{
code: '<div onMouseOver={() => void 0} onFocus={() => void 0} {...props} />;',
},
{ code: '<div onMouseOver={handleMouseOver} onFocus={handleFocus} />;' },
{
code: '<div onMouseOver={handleMouseOver} onFocus={handleFocus} {...props} />;',
},
{ code: '<div />;' },
{ code: '<div onBlur={() => {}} />' },
{ code: '<div onFocus={() => {}} />' },
{ code: '<div onMouseOut={() => void 0} onBlur={() => void 0} />' },
{ code: '<div onMouseOut={() => void 0} onBlur={() => void 0} {...props} />' },
{ code: '<div onMouseOut={handleMouseOut} onBlur={handleOnBlur} />' },
{ code: '<div onMouseOut={handleMouseOut} onBlur={handleOnBlur} {...props} />' },
{ code: '<MyElement />' },
{ code: '<MyElement onMouseOver={() => {}} />' },
{ code: '<MyElement onMouseOut={() => {}} />' },
{ code: '<MyElement onBlur={() => {}} />' },
{ code: '<MyElement onFocus={() => {}} />' },
{ code: '<MyElement onMouseOver={() => {}} {...props} />' },
{ code: '<MyElement onMouseOut={() => {}} {...props} />' },
{ code: '<MyElement onBlur={() => {}} {...props} />' },
{ code: '<MyElement onFocus={() => {}} {...props} />' },
/* Passing in empty options doesn't check any event handlers */
{
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
options: [{ hoverInHandlers: [], hoverOutHandlers: [] }],
},
/* Passing in custom handlers */
{
code: '<div onMouseOver={() => {}} onFocus={() => {}} />',
options: [{ hoverInHandlers: ['onMouseOver'] }],
},
{
code: '<div onMouseEnter={() => {}} onFocus={() => {}} />',
options: [{ hoverInHandlers: ['onMouseEnter'] }],
},
{
code: '<div onMouseOut={() => {}} onBlur={() => {}} />',
options: [{ hoverOutHandlers: ['onMouseOut'] }],
},
{
code: '<div onMouseLeave={() => {}} onBlur={() => {}} />',
options: [{ hoverOutHandlers: ['onMouseLeave'] }],
},
{
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
options: [
{ hoverInHandlers: ['onPointerEnter'], hoverOutHandlers: ['onPointerLeave'] },
],
},
/* Custom options only checks the handlers passed in */
{
code: '<div onMouseLeave={() => {}} />',
options: [{ hoverOutHandlers: ['onPointerLeave'] }],
},
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div onMouseOver={() => void 0} />;', errors: [mouseOverError] },
{ code: '<div onMouseOut={() => void 0} />', errors: [mouseOutError] },
{
code: '<div onMouseOver={() => void 0} onFocus={undefined} />;',
errors: [mouseOverError],
},
{
code: '<div onMouseOut={() => void 0} onBlur={undefined} />',
errors: [mouseOutError],
},
{
code: '<div onMouseOver={() => void 0} {...props} />',
errors: [mouseOverError],
},
{
code: '<div onMouseOut={() => void 0} {...props} />',
errors: [mouseOutError],
},
/* Custom options */
{
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
options: [
{ hoverInHandlers: ['onMouseOver'], hoverOutHandlers: ['onMouseOut'] },
],
errors: [mouseOverError, mouseOutError],
},
{
code: '<div onPointerEnter={() => {}} onPointerLeave={() => {}} />',
options: [
{ hoverInHandlers: ['onPointerEnter'], hoverOutHandlers: ['onPointerLeave'] },
],
errors: [pointerEnterError, pointerLeaveError],
},
{
code: '<div onMouseOver={() => {}} />',
options: [{ hoverInHandlers: ['onMouseOver'] }],
errors: [mouseOverError],
},
{
code: '<div onPointerEnter={() => {}} />',
options: [{ hoverInHandlers: ['onPointerEnter'] }],
errors: [pointerEnterError],
},
{
code: '<div onMouseOut={() => {}} />',
options: [{ hoverOutHandlers: ['onMouseOut'] }],
errors: [mouseOutError],
},
{
code: '<div onPointerLeave={() => {}} />',
options: [{ hoverOutHandlers: ['onPointerLeave'] }],
errors: [pointerLeaveError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,48 @@
/**
* @fileoverview Enforce no accesskey attribute on element.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-access-key';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'No access key attribute allowed. Inconsistencies between keyboard shortcuts and keyboard commands used by screenreaders and keyboard-only users create a11y complications.',
type: 'JSXOpeningElement',
};
ruleTester.run('no-access-key', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<div {...props} />' },
{ code: '<div accessKey={undefined} />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div accesskey="h" />', errors: [expectedError] },
{ code: '<div accessKey="h" />', errors: [expectedError] },
{ code: '<div accessKey="h" {...props} />', errors: [expectedError] },
{ code: '<div acCesSKeY="y" />', errors: [expectedError] },
{ code: '<div accessKey={"y"} />', errors: [expectedError] },
{ code: '<div accessKey={`${y}`} />', errors: [expectedError] },
{
code: '<div accessKey={`${undefined}y${undefined}`} />',
errors: [expectedError],
},
{ code: '<div accessKey={`This is ${bad}`} />', errors: [expectedError] },
{ code: '<div accessKey={accessKey} />', errors: [expectedError] },
{ code: '<div accessKey={`${undefined}`} />', errors: [expectedError] },
{ code: '<div accessKey={`${undefined}${undefined}`} />', errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,44 @@
/**
* @fileoverview Enforce `aria-hidden="true"` is not used on focusable elements.
* @author Kate Higa
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-aria-hidden-on-focusable';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'aria-hidden="true" must not be set on focusable elements.',
type: 'JSXOpeningElement',
};
ruleTester.run('no-aria-hidden-on-focusable', rule, {
valid: parsers.all([].concat(
{ code: '<div aria-hidden="true" />;' },
{ code: '<div onClick={() => void 0} aria-hidden="true" />;' },
{ code: '<img aria-hidden="true" />' },
{ code: '<a aria-hidden="false" href="#" />' },
{ code: '<button aria-hidden="true" tabIndex="-1" />' },
{ code: '<button />' },
{ code: '<a href="/" />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div aria-hidden="true" tabIndex="0" />;', errors: [expectedError] },
{ code: '<input aria-hidden="true" />;', errors: [expectedError] },
{ code: '<a href="/" aria-hidden="true" />', errors: [expectedError] },
{ code: '<button aria-hidden="true" />', errors: [expectedError] },
{ code: '<textarea aria-hidden="true" />', errors: [expectedError] },
{ code: '<p tabindex="0" aria-hidden="true">text</p>;', errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,68 @@
/**
* @fileoverview Enforce autoFocus prop is not used.
* @author Ethan Cohen <@evcohen>
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-autofocus';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'The autoFocus prop should not be used, as it can reduce usability and accessibility for users.',
type: 'JSXAttribute',
};
const ignoreNonDOMSchema = [
{
ignoreNonDOM: true,
},
];
const componentsSettings = {
'jsx-a11y': {
components: {
Button: 'button',
},
},
};
ruleTester.run('no-autofocus', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<div autofocus />;' },
{ code: '<input autofocus="true" />;' },
{ code: '<Foo bar />' },
{ code: '<Foo autoFocus />', options: ignoreNonDOMSchema },
{ code: '<div><div autofocus /></div>', options: ignoreNonDOMSchema },
{ code: '<Button />', settings: componentsSettings },
{ code: '<Button />', options: ignoreNonDOMSchema, settings: componentsSettings },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div autoFocus />', errors: [expectedError] },
{ code: '<div autoFocus={true} />', errors: [expectedError] },
{ code: '<div autoFocus={false} />', errors: [expectedError] },
{ code: '<div autoFocus={undefined} />', errors: [expectedError] },
{ code: '<div autoFocus="true" />', errors: [expectedError] },
{ code: '<div autoFocus="false" />', errors: [expectedError] },
{ code: '<input autoFocus />', errors: [expectedError] },
{ code: '<Foo autoFocus />', errors: [expectedError] },
{ code: '<Button autoFocus />', errors: [expectedError], settings: componentsSettings },
{
code: '<Button autoFocus />',
errors: [expectedError],
options: ignoreNonDOMSchema,
settings: componentsSettings,
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,51 @@
/**
* @fileoverview Enforce distracting elements are not used.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-distracting-elements';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = (element) => ({
message: `Do not use <${element}> elements as they can create visual accessibility issues and are deprecated.`,
type: 'JSXOpeningElement',
});
const componentsSettings = {
'jsx-a11y': {
components: {
Blink: 'blink',
},
},
};
ruleTester.run('no-marquee', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<Marquee />' },
{ code: '<div marquee />' },
{ code: '<Blink />' },
{ code: '<div blink />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<marquee />', errors: [expectedError('marquee')] },
{ code: '<marquee {...props} />', errors: [expectedError('marquee')] },
{ code: '<marquee lang={undefined} />', errors: [expectedError('marquee')] },
{ code: '<blink />', errors: [expectedError('blink')] },
{ code: '<blink {...props} />', errors: [expectedError('blink')] },
{ code: '<blink foo={undefined} />', errors: [expectedError('blink')] },
{ code: '<Blink />', settings: componentsSettings, errors: [expectedError('blink')] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,405 @@
/**
* @fileoverview Disallow inherently interactive elements to be assigned
* non-interactive roles.
* @author Jesse Beach
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-interactive-element-to-noninteractive-role';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = 'Interactive elements should not be assigned non-interactive roles.';
const expectedError = {
message: errorMessage,
type: 'JSXAttribute',
};
const ruleName = 'jsx-a11y/no-interactive-element-to-noninteractive-role';
const componentsSettings = {
'jsx-a11y': {
components: {
Button: 'button',
Link: 'a',
},
},
};
const alwaysValid = [
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
/* Interactive elements */
{ code: '<a href="http://x.y.z" role="button" />' },
{ code: '<a href="http://x.y.z" tabIndex="0" role="button" />' },
{ code: '<button className="foo" role="button" />' },
/* All flavors of input */
{ code: '<input role="button" />' },
{ code: '<input type="button" role="button" />' },
{ code: '<input type="checkbox" role="button" />' },
{ code: '<input type="color" role="button" />' },
{ code: '<input type="date" role="button" />' },
{ code: '<input type="datetime" role="button" />' },
{ code: '<input type="datetime-local" role="button" />' },
{ code: '<input type="email" role="button" />' },
{ code: '<input type="file" role="button" />' },
{ code: '<input type="image" role="button" />' },
{ code: '<input type="month" role="button" />' },
{ code: '<input type="number" role="button" />' },
{ code: '<input type="password" role="button" />' },
{ code: '<input type="radio" role="button" />' },
{ code: '<input type="range" role="button" />' },
{ code: '<input type="reset" role="button" />' },
{ code: '<input type="search" role="button" />' },
{ code: '<input type="submit" role="button" />' },
{ code: '<input type="tel" role="button" />' },
{ code: '<input type="text" role="button" />' },
{ code: '<input type="time" role="button" />' },
{ code: '<input type="url" role="button" />' },
{ code: '<input type="week" role="button" />' },
{ code: '<input type="hidden" role="button" />' },
/* End all flavors of input */
{ code: '<menuitem role="button" />;' },
{ code: '<option className="foo" role="button" />' },
{ code: '<select className="foo" role="button" />' },
{ code: '<textarea className="foo" role="button" />' },
{ code: '<tr role="button" />;' },
/* HTML elements with neither an interactive or non-interactive valence (static) */
{ code: '<a role="button" />' },
{ code: '<a role="img" />;' },
{ code: '<a tabIndex="0" role="button" />' },
{ code: '<a tabIndex="0" role="img" />' },
{ code: '<acronym role="button" />;' },
{ code: '<address role="button" />;' },
{ code: '<applet role="button" />;' },
{ code: '<aside role="button" />;' },
{ code: '<audio role="button" />;' },
{ code: '<b role="button" />;' },
{ code: '<base role="button" />;' },
{ code: '<bdi role="button" />;' },
{ code: '<bdo role="button" />;' },
{ code: '<big role="button" />;' },
{ code: '<blink role="button" />;' },
{ code: '<blockquote role="button" />;' },
{ code: '<body role="button" />;' },
{ code: '<br role="button" />;' },
{ code: '<canvas role="button" />;' },
{ code: '<caption role="button" />;' },
{ code: '<center role="button" />;' },
{ code: '<cite role="button" />;' },
{ code: '<code role="button" />;' },
{ code: '<col role="button" />;' },
{ code: '<colgroup role="button" />;' },
{ code: '<content role="button" />;' },
{ code: '<data role="button" />;' },
{ code: '<datalist role="button" />;' },
{ code: '<del role="button" />;' },
{ code: '<details role="button" />;' },
{ code: '<dir role="button" />;' },
{ code: '<div role="button" />;' },
{ code: '<div className="foo" role="button" />;' },
{ code: '<div className="foo" {...props} role="button" />;' },
{ code: '<div aria-hidden role="button" />;' },
{ code: '<div aria-hidden={true} role="button" />;' },
{ code: '<div role="button" />;' },
{ code: '<div role={undefined} role="button" />;' },
{ code: '<div {...props} role="button" />;' },
{ code: '<div onKeyUp={() => void 0} aria-hidden={false} role="button" />;' },
{ code: '<dl role="button" />;' },
{ code: '<em role="button" />;' },
{ code: '<embed role="button" />;' },
{ code: '<figcaption role="button" />;' },
{ code: '<font role="button" />;' },
{ code: '<footer role="button" />;' },
{ code: '<frameset role="button" />;' },
{ code: '<head role="button" />;' },
{ code: '<header role="button" />;' },
{ code: '<hgroup role="button" />;' },
{ code: '<html role="button" />;' },
{ code: '<i role="button" />;' },
{ code: '<iframe role="button" />;' },
{ code: '<ins role="button" />;' },
{ code: '<kbd role="button" />;' },
{ code: '<keygen role="button" />;' },
{ code: '<label role="button" />;' },
{ code: '<legend role="button" />;' },
{ code: '<link role="button" />;' },
{ code: '<map role="button" />;' },
{ code: '<mark role="button" />;' },
{ code: '<marquee role="button" />;' },
{ code: '<menu role="button" />;' },
{ code: '<meta role="button" />;' },
{ code: '<meter role="button" />;' },
{ code: '<noembed role="button" />;' },
{ code: '<noscript role="button" />;' },
{ code: '<object role="button" />;' },
{ code: '<optgroup role="button" />;' },
{ code: '<output role="button" />;' },
{ code: '<p role="button" />;' },
{ code: '<param role="button" />;' },
{ code: '<picture role="button" />;' },
{ code: '<pre role="button" />;' },
{ code: '<progress role="button" />;' },
{ code: '<q role="button" />;' },
{ code: '<rp role="button" />;' },
{ code: '<rt role="button" />;' },
{ code: '<rtc role="button" />;' },
{ code: '<ruby role="button" />;' },
{ code: '<s role="button" />;' },
{ code: '<samp role="button" />;' },
{ code: '<script role="button" />;' },
{ code: '<section role="button" />;' },
{ code: '<small role="button" />;' },
{ code: '<source role="button" />;' },
{ code: '<spacer role="button" />;' },
{ code: '<span role="button" />;' },
{ code: '<strike role="button" />;' },
{ code: '<strong role="button" />;' },
{ code: '<style role="button" />;' },
{ code: '<sub role="button" />;' },
{ code: '<summary role="button" />;' },
{ code: '<sup role="button" />;' },
{ code: '<th role="button" />;' },
{ code: '<time role="button" />;' },
{ code: '<title role="button" />;' },
{ code: '<track role="button" />;' },
{ code: '<tt role="button" />;' },
{ code: '<u role="button" />;' },
{ code: '<var role="button" />;' },
{ code: '<video role="button" />;' },
{ code: '<wbr role="button" />;' },
{ code: '<xmp role="button" />;' },
/* HTML elements attributed with an interactive role */
{ code: '<div role="button" />;' },
{ code: '<div role="checkbox" />;' },
{ code: '<div role="columnheader" />;' },
{ code: '<div role="combobox" />;' },
{ code: '<div role="grid" />;' },
{ code: '<div role="gridcell" />;' },
{ code: '<div role="link" />;' },
{ code: '<div role="listbox" />;' },
{ code: '<div role="menu" />;' },
{ code: '<div role="menubar" />;' },
{ code: '<div role="menuitem" />;' },
{ code: '<div role="menuitemcheckbox" />;' },
{ code: '<div role="menuitemradio" />;' },
{ code: '<div role="option" />;' },
{ code: '<div role="progressbar" />;' },
{ code: '<div role="radio" />;' },
{ code: '<div role="radiogroup" />;' },
{ code: '<div role="row" />;' },
{ code: '<div role="rowheader" />;' },
{ code: '<div role="searchbox" />;' },
{ code: '<div role="slider" />;' },
{ code: '<div role="spinbutton" />;' },
{ code: '<div role="switch" />;' },
{ code: '<div role="tab" />;' },
{ code: '<div role="textbox" />;' },
{ code: '<div role="treeitem" />;' },
/* Presentation is a special case role that indicates intentional static semantics */
{ code: '<div role="presentation" />;' },
/* HTML elements attributed with an abstract role */
{ code: '<div role="command" />;' },
{ code: '<div role="composite" />;' },
{ code: '<div role="input" />;' },
{ code: '<div role="landmark" />;' },
{ code: '<div role="range" />;' },
{ code: '<div role="roletype" />;' },
{ code: '<div role="section" />;' },
{ code: '<div role="sectionhead" />;' },
{ code: '<div role="select" />;' },
{ code: '<div role="structure" />;' },
{ code: '<div role="tablist" />;' },
{ code: '<div role="toolbar" />;' },
{ code: '<div role="tree" />;' },
{ code: '<div role="treegrid" />;' },
{ code: '<div role="widget" />;' },
{ code: '<div role="window" />;' },
/* HTML elements with an inherent, non-interactive role, assigned an
* interactive role. */
{ code: '<main role="button" />;' },
{ code: '<area role="button" />;' },
{ code: '<article role="button" />;' },
{ code: '<article role="button" />;' },
{ code: '<dd role="button" />;' },
{ code: '<dfn role="button" />;' },
{ code: '<dt role="button" />;' },
{ code: '<fieldset role="button" />;' },
{ code: '<figure role="button" />;' },
{ code: '<form role="button" />;' },
{ code: '<frame role="button" />;' },
{ code: '<h1 role="button" />;' },
{ code: '<h2 role="button" />;' },
{ code: '<h3 role="button" />;' },
{ code: '<h4 role="button" />;' },
{ code: '<h5 role="button" />;' },
{ code: '<h6 role="button" />;' },
{ code: '<hr role="button" />;' },
{ code: '<img role="button" />;' },
{ code: '<li role="button" />;' },
{ code: '<li role="presentation" />;' },
{ code: '<nav role="button" />;' },
{ code: '<ol role="button" />;' },
{ code: '<table role="button" />;' },
{ code: '<tbody role="button" />;' },
{ code: '<td role="button" />;' },
{ code: '<tfoot role="button" />;' },
{ code: '<thead role="button" />;' },
{ code: '<ul role="button" />;' },
/* HTML elements attributed with a non-interactive role */
{ code: '<div role="alert" />;' },
{ code: '<div role="alertdialog" />;' },
{ code: '<div role="application" />;' },
{ code: '<div role="article" />;' },
{ code: '<div role="banner" />;' },
{ code: '<div role="cell" />;' },
{ code: '<div role="complementary" />;' },
{ code: '<div role="contentinfo" />;' },
{ code: '<div role="definition" />;' },
{ code: '<div role="dialog" />;' },
{ code: '<div role="directory" />;' },
{ code: '<div role="document" />;' },
{ code: '<div role="feed" />;' },
{ code: '<div role="figure" />;' },
{ code: '<div role="form" />;' },
{ code: '<div role="group" />;' },
{ code: '<div role="heading" />;' },
{ code: '<div role="img" />;' },
{ code: '<div role="list" />;' },
{ code: '<div role="listitem" />;' },
{ code: '<div role="log" />;' },
{ code: '<div role="main" />;' },
{ code: '<div role="marquee" />;' },
{ code: '<div role="math" />;' },
{ code: '<div role="navigation" />;' },
{ code: '<div role="note" />;' },
{ code: '<div role="region" />;' },
{ code: '<div role="rowgroup" />;' },
{ code: '<div role="search" />;' },
{ code: '<div role="separator" />;' },
{ code: '<div role="scrollbar" />;' },
{ code: '<div role="status" />;' },
{ code: '<div role="table" />;' },
{ code: '<div role="tabpanel" />;' },
{ code: '<div role="term" />;' },
{ code: '<div role="timer" />;' },
{ code: '<div role="tooltip" />;' },
/* Namespaced roles are not checked */
{ code: '<div mynamespace:role="term" />' },
{ code: '<input mynamespace:role="img" />' },
{ code: '<Link href="http://x.y.z" role="img" />' },
{ code: '<Link href="http://x.y.z" />', settings: componentsSettings },
{ code: '<Button onClick={doFoo} />', settings: componentsSettings },
];
const neverValid = [
/* Interactive elements */
{ code: '<a href="http://x.y.z" role="img" />', errors: [expectedError] },
{ code: '<a href="http://x.y.z" tabIndex="0" role="img" />', errors: [expectedError] },
/* All flavors of input */
{ code: '<input role="img" />', errors: [expectedError] },
{ code: '<input type="img" role="img" />', errors: [expectedError] },
{ code: '<input type="checkbox" role="img" />', errors: [expectedError] },
{ code: '<input type="color" role="img" />', errors: [expectedError] },
{ code: '<input type="date" role="img" />', errors: [expectedError] },
{ code: '<input type="datetime" role="img" />', errors: [expectedError] },
{ code: '<input type="datetime-local" role="img" />', errors: [expectedError] },
{ code: '<input type="email" role="img" />', errors: [expectedError] },
{ code: '<input type="file" role="img" />', errors: [expectedError] },
{ code: '<input type="hidden" role="img" />', errors: [expectedError] },
{ code: '<input type="image" role="img" />', errors: [expectedError] },
{ code: '<input type="month" role="img" />', errors: [expectedError] },
{ code: '<input type="number" role="img" />', errors: [expectedError] },
{ code: '<input type="password" role="img" />', errors: [expectedError] },
{ code: '<input type="radio" role="img" />', errors: [expectedError] },
{ code: '<input type="range" role="img" />', errors: [expectedError] },
{ code: '<input type="reset" role="img" />', errors: [expectedError] },
{ code: '<input type="search" role="img" />', errors: [expectedError] },
{ code: '<input type="submit" role="img" />', errors: [expectedError] },
{ code: '<input type="tel" role="img" />', errors: [expectedError] },
{ code: '<input type="text" role="img" />', errors: [expectedError] },
{ code: '<input type="time" role="img" />', errors: [expectedError] },
{ code: '<input type="url" role="img" />', errors: [expectedError] },
{ code: '<input type="week" role="img" />', errors: [expectedError] },
/* End all flavors of input */
{ code: '<menuitem role="img" />;', errors: [expectedError] },
{ code: '<option className="foo" role="img" />', errors: [expectedError] },
{ code: '<select className="foo" role="img" />', errors: [expectedError] },
{ code: '<textarea className="foo" role="img" />', errors: [expectedError] },
{ code: '<tr role="img" />;', errors: [expectedError] },
/* Interactive elements */
{ code: '<a href="http://x.y.z" role="listitem" />', errors: [expectedError] },
{ code: '<a href="http://x.y.z" tabIndex="0" role="listitem" />', errors: [expectedError] },
/* All flavors of input */
{ code: '<input role="listitem" />', errors: [expectedError] },
{ code: '<input type="listitem" role="listitem" />', errors: [expectedError] },
{ code: '<input type="checkbox" role="listitem" />', errors: [expectedError] },
{ code: '<input type="color" role="listitem" />', errors: [expectedError] },
{ code: '<input type="date" role="listitem" />', errors: [expectedError] },
{ code: '<input type="datetime" role="listitem" />', errors: [expectedError] },
{ code: '<input type="datetime-local" role="listitem" />', errors: [expectedError] },
{ code: '<input type="email" role="listitem" />', errors: [expectedError] },
{ code: '<input type="file" role="listitem" />', errors: [expectedError] },
{ code: '<input type="image" role="listitem" />', errors: [expectedError] },
{ code: '<input type="month" role="listitem" />', errors: [expectedError] },
{ code: '<input type="number" role="listitem" />', errors: [expectedError] },
{ code: '<input type="password" role="listitem" />', errors: [expectedError] },
{ code: '<input type="radio" role="listitem" />', errors: [expectedError] },
{ code: '<input type="range" role="listitem" />', errors: [expectedError] },
{ code: '<input type="reset" role="listitem" />', errors: [expectedError] },
{ code: '<input type="search" role="listitem" />', errors: [expectedError] },
{ code: '<input type="submit" role="listitem" />', errors: [expectedError] },
{ code: '<input type="tel" role="listitem" />', errors: [expectedError] },
{ code: '<input type="text" role="listitem" />', errors: [expectedError] },
{ code: '<input type="time" role="listitem" />', errors: [expectedError] },
{ code: '<input type="url" role="listitem" />', errors: [expectedError] },
{ code: '<input type="week" role="listitem" />', errors: [expectedError] },
/* End all flavors of input */
{ code: '<menuitem role="listitem" />;', errors: [expectedError] },
{ code: '<option className="foo" role="listitem" />', errors: [expectedError] },
{ code: '<select className="foo" role="listitem" />', errors: [expectedError] },
{ code: '<textarea className="foo" role="listitem" />', errors: [expectedError] },
{ code: '<tr role="listitem" />;', errors: [expectedError] },
/* Custom elements */
{ code: '<Link href="http://x.y.z" role="img" />', errors: [expectedError], settings: componentsSettings },
];
const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
{ code: '<tr role="presentation" />;' },
{ code: '<canvas role="img" />;' },
{ code: '<Component role="presentation" />;' },
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
{ code: '<tr role="presentation" />;', errors: [expectedError] },
{ code: '<canvas role="img" />;', errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,502 @@
/**
* @fileoverview Enforce non-interactive elements have no interactive handlers.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-noninteractive-element-interactions';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = 'Non-interactive elements should not be assigned mouse or keyboard event listeners.';
const expectedError = {
message: errorMessage,
type: 'JSXOpeningElement',
};
const ruleName = 'no-noninteractive-element-interactions';
const alwaysValid = [
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
{ code: '<Image onClick={() => void 0} />;' },
{
code: '<Button onClick={() => void 0} />;',
settings: { 'jsx-a11y': { components: { Button: 'button' } } },
},
/* All flavors of input */
{ code: '<input onClick={() => void 0} />' },
{ code: '<input type="button" onClick={() => void 0} />' },
{ code: '<input type="checkbox" onClick={() => void 0} />' },
{ code: '<input type="color" onClick={() => void 0} />' },
{ code: '<input type="date" onClick={() => void 0} />' },
{ code: '<input type="datetime" onClick={() => void 0} />' },
{ code: '<input type="datetime-local" onClick={() => void 0} />' },
{ code: '<input type="email" onClick={() => void 0} />' },
{ code: '<input type="file" onClick={() => void 0} />' },
{ code: '<input type="image" onClick={() => void 0} />' },
{ code: '<input type="month" onClick={() => void 0} />' },
{ code: '<input type="number" onClick={() => void 0} />' },
{ code: '<input type="password" onClick={() => void 0} />' },
{ code: '<input type="radio" onClick={() => void 0} />' },
{ code: '<input type="range" onClick={() => void 0} />' },
{ code: '<input type="reset" onClick={() => void 0} />' },
{ code: '<input type="search" onClick={() => void 0} />' },
{ code: '<input type="submit" onClick={() => void 0} />' },
{ code: '<input type="tel" onClick={() => void 0} />' },
{ code: '<input type="text" onClick={() => void 0} />' },
{ code: '<input type="time" onClick={() => void 0} />' },
{ code: '<input type="url" onClick={() => void 0} />' },
{ code: '<input type="week" onClick={() => void 0} />' },
{ code: '<input type="hidden" onClick={() => void 0} />' },
/* End all flavors of input */
{ code: '<a onClick={() => void 0} />' },
{ code: '<a onClick={() => {}} />;' },
{ code: '<a tabIndex="0" onClick={() => void 0} />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
{ code: '<area onClick={() => {}} />;' },
{ code: '<button onClick={() => void 0} className="foo" />' },
{ code: '<menuitem onClick={() => {}} />;' },
{ code: '<option onClick={() => void 0} className="foo" />' },
{ code: '<select onClick={() => void 0} className="foo" />' },
{ code: '<textarea onClick={() => void 0} className="foo" />' },
{ code: '<tr onClick={() => {}} />;' },
/* HTML elements with neither an interactive or non-interactive valence (static) */
{ code: '<acronym onClick={() => {}} />;' },
{ code: '<address onClick={() => {}} />;' },
{ code: '<applet onClick={() => {}} />;' },
{ code: '<audio onClick={() => {}} />;' },
{ code: '<b onClick={() => {}} />;' },
{ code: '<base onClick={() => {}} />;' },
{ code: '<bdi onClick={() => {}} />;' },
{ code: '<bdo onClick={() => {}} />;' },
{ code: '<big onClick={() => {}} />;' },
{ code: '<blink onClick={() => {}} />;' },
{ code: '<body onLoad={() => {}} />;' },
{ code: '<canvas onClick={() => {}} />;' },
{ code: '<center onClick={() => {}} />;' },
{ code: '<cite onClick={() => {}} />;' },
{ code: '<code onClick={() => {}} />;' },
{ code: '<col onClick={() => {}} />;' },
{ code: '<colgroup onClick={() => {}} />;' },
{ code: '<content onClick={() => {}} />;' },
{ code: '<data onClick={() => {}} />;' },
{ code: '<datalist onClick={() => {}} />;' },
{ code: '<del onClick={() => {}} />;' },
{ code: '<div />;' },
{ code: '<div className="foo" />;' },
{ code: '<div className="foo" {...props} />;' },
{ code: '<div onClick={() => void 0} aria-hidden />;' },
{ code: '<div onClick={() => void 0} aria-hidden={true} />;' },
{ code: '<div onClick={() => void 0} />;' },
{ code: '<div onClick={() => void 0} role={undefined} />;' },
{ code: '<div onClick={() => void 0} {...props} />;' },
{ code: '<div onClick={null} />;' },
{ code: '<div onKeyUp={() => void 0} aria-hidden={false} />;' },
{ code: '<em onClick={() => {}} />;' },
{ code: '<embed onClick={() => {}} />;' },
{ code: '<font onClick={() => {}} />;' },
{ code: '<font onSubmit={() => {}} />;' },
{ code: '<form onSubmit={() => {}} />;' },
{
code: `
<form onSubmit={this.handleSubmit.bind(this)} method="POST">
<button type="submit">
Save
</button>
</form>
`,
},
{ code: '<frameset onClick={() => {}} />;' },
{ code: '<head onClick={() => {}} />;' },
{ code: '<header onClick={() => {}} />;' },
{ code: '<hgroup onClick={() => {}} />;' },
{ code: '<html onClick={() => {}} />;' },
{ code: '<i onClick={() => {}} />;' },
{ code: '<iframe onLoad={() => {}} />;' },
{
code: `
<iframe
name="embeddedExternalPayment"
ref="embeddedExternalPayment"
style={iframeStyle}
onLoad={this.handleLoadIframe}
/>
`,
},
{ code: '<img {...props} onError={() => {}} />;' },
{ code: '<img onLoad={() => {}} />;' },
{ code: '<img src={currentPhoto.imageUrl} onLoad={this.handleImageLoad} alt="for review" />' },
{
code: `
<img
ref={this.ref}
className="c-responsive-image-placeholder__image"
src={src}
alt={alt}
data-test-id="test-id"
onLoad={this.fetchCompleteImage}
/>
`,
},
{ code: '<ins onClick={() => {}} />;' },
{ code: '<kbd onClick={() => {}} />;' },
{ code: '<keygen onClick={() => {}} />;' },
{ code: '<link onClick={() => {}} />;' },
{ code: '<main onClick={null} />;' },
{ code: '<map onClick={() => {}} />;' },
{ code: '<meta onClick={() => {}} />;' },
{ code: '<noembed onClick={() => {}} />;' },
{ code: '<noscript onClick={() => {}} />;' },
{ code: '<object onClick={() => {}} />;' },
{ code: '<param onClick={() => {}} />;' },
{ code: '<picture onClick={() => {}} />;' },
{ code: '<q onClick={() => {}} />;' },
{ code: '<rp onClick={() => {}} />;' },
{ code: '<rt onClick={() => {}} />;' },
{ code: '<rtc onClick={() => {}} />;' },
{ code: '<s onClick={() => {}} />;' },
{ code: '<samp onClick={() => {}} />;' },
{ code: '<script onClick={() => {}} />;' },
{ code: '<section onClick={() => {}} />;' },
{ code: '<small onClick={() => {}} />;' },
{ code: '<source onClick={() => {}} />;' },
{ code: '<spacer onClick={() => {}} />;' },
{ code: '<span onClick={() => {}} />;' },
{ code: '<strike onClick={() => {}} />;' },
{ code: '<strong onClick={() => {}} />;' },
{ code: '<style onClick={() => {}} />;' },
{ code: '<sub onClick={() => {}} />;' },
{ code: '<summary onClick={() => {}} />;' },
{ code: '<sup onClick={() => {}} />;' },
{ code: '<th onClick={() => {}} />;' },
{ code: '<title onClick={() => {}} />;' },
{ code: '<track onClick={() => {}} />;' },
{ code: '<tt onClick={() => {}} />;' },
{ code: '<u onClick={() => {}} />;' },
{ code: '<var onClick={() => {}} />;' },
{ code: '<video onClick={() => {}} />;' },
{ code: '<wbr onClick={() => {}} />;' },
{ code: '<xmp onClick={() => {}} />;' },
/* HTML elements attributed with an interactive role */
{ code: '<div role="button" onClick={() => {}} />;' },
{ code: '<div role="checkbox" onClick={() => {}} />;' },
{ code: '<div role="columnheader" onClick={() => {}} />;' },
{ code: '<div role="combobox" onClick={() => {}} />;' },
{ code: '<div role="grid" onClick={() => {}} />;' },
{ code: '<div role="gridcell" onClick={() => {}} />;' },
{ code: '<div role="link" onClick={() => {}} />;' },
{ code: '<div role="listbox" onClick={() => {}} />;' },
{ code: '<div role="menu" onClick={() => {}} />;' },
{ code: '<div role="menubar" onClick={() => {}} />;' },
{ code: '<div role="menuitem" onClick={() => {}} />;' },
{ code: '<div role="menuitemcheckbox" onClick={() => {}} />;' },
{ code: '<div role="menuitemradio" onClick={() => {}} />;' },
{ code: '<div role="option" onClick={() => {}} />;' },
{ code: '<div role="progressbar" onClick={() => {}} />;' },
{ code: '<div role="radio" onClick={() => {}} />;' },
{ code: '<div role="radiogroup" onClick={() => {}} />;' },
{ code: '<div role="row" onClick={() => {}} />;' },
{ code: '<div role="rowheader" onClick={() => {}} />;' },
{ code: '<div role="scrollbar" onClick={() => {}} />;' },
{ code: '<div role="searchbox" onClick={() => {}} />;' },
{ code: '<div role="slider" onClick={() => {}} />;' },
{ code: '<div role="spinbutton" onClick={() => {}} />;' },
{ code: '<div role="switch" onClick={() => {}} />;' },
{ code: '<div role="tab" onClick={() => {}} />;' },
{ code: '<div role="textbox" onClick={() => {}} />;' },
{ code: '<div role="treeitem" onClick={() => {}} />;' },
/* Presentation is a special case role that indicates intentional static semantics */
{ code: '<div role="presentation" onClick={() => {}} />;' },
/* HTML elements attributed with an abstract role */
{ code: '<div role="command" onClick={() => {}} />;' },
{ code: '<div role="composite" onClick={() => {}} />;' },
{ code: '<div role="input" onClick={() => {}} />;' },
{ code: '<div role="landmark" onClick={() => {}} />;' },
{ code: '<div role="range" onClick={() => {}} />;' },
{ code: '<div role="roletype" onClick={() => {}} />;' },
{ code: '<div role="sectionhead" onClick={() => {}} />;' },
{ code: '<div role="select" onClick={() => {}} />;' },
{ code: '<div role="structure" onClick={() => {}} />;' },
{ code: '<div role="tablist" onClick={() => {}} />;' },
{ code: '<div role="toolbar" onClick={() => {}} />;' },
{ code: '<div role="tree" onClick={() => {}} />;' },
{ code: '<div role="treegrid" onClick={() => {}} />;' },
{ code: '<div role="widget" onClick={() => {}} />;' },
{ code: '<div role="window" onClick={() => {}} />;' },
// All the possible handlers
{ code: '<div role="article" onCopy={() => {}} />;' },
{ code: '<div role="article" onCut={() => {}} />;' },
{ code: '<div role="article" onPaste={() => {}} />;' },
{ code: '<div role="article" onCompositionEnd={() => {}} />;' },
{ code: '<div role="article" onCompositionStart={() => {}} />;' },
{ code: '<div role="article" onCompositionUpdate={() => {}} />;' },
{ code: '<div role="article" onChange={() => {}} />;' },
{ code: '<div role="article" onInput={() => {}} />;' },
{ code: '<div role="article" onSubmit={() => {}} />;' },
{ code: '<div role="article" onSelect={() => {}} />;' },
{ code: '<div role="article" onTouchCancel={() => {}} />;' },
{ code: '<div role="article" onTouchEnd={() => {}} />;' },
{ code: '<div role="article" onTouchMove={() => {}} />;' },
{ code: '<div role="article" onTouchStart={() => {}} />;' },
{ code: '<div role="article" onScroll={() => {}} />;' },
{ code: '<div role="article" onWheel={() => {}} />;' },
{ code: '<div role="article" onAbort={() => {}} />;' },
{ code: '<div role="article" onCanPlay={() => {}} />;' },
{ code: '<div role="article" onCanPlayThrough={() => {}} />;' },
{ code: '<div role="article" onDurationChange={() => {}} />;' },
{ code: '<div role="article" onEmptied={() => {}} />;' },
{ code: '<div role="article" onEncrypted={() => {}} />;' },
{ code: '<div role="article" onEnded={() => {}} />;' },
{ code: '<div role="article" onLoadedData={() => {}} />;' },
{ code: '<div role="article" onLoadedMetadata={() => {}} />;' },
{ code: '<div role="article" onLoadStart={() => {}} />;' },
{ code: '<div role="article" onPause={() => {}} />;' },
{ code: '<div role="article" onPlay={() => {}} />;' },
{ code: '<div role="article" onPlaying={() => {}} />;' },
{ code: '<div role="article" onProgress={() => {}} />;' },
{ code: '<div role="article" onRateChange={() => {}} />;' },
{ code: '<div role="article" onSeeked={() => {}} />;' },
{ code: '<div role="article" onSeeking={() => {}} />;' },
{ code: '<div role="article" onStalled={() => {}} />;' },
{ code: '<div role="article" onSuspend={() => {}} />;' },
{ code: '<div role="article" onTimeUpdate={() => {}} />;' },
{ code: '<div role="article" onVolumeChange={() => {}} />;' },
{ code: '<div role="article" onWaiting={() => {}} />;' },
{ code: '<div role="article" onAnimationStart={() => {}} />;' },
{ code: '<div role="article" onAnimationEnd={() => {}} />;' },
{ code: '<div role="article" onAnimationIteration={() => {}} />;' },
{ code: '<div role="article" onTransitionEnd={() => {}} />;' },
];
const neverValid = [
/* HTML elements with an inherent, non-interactive role */
{ code: '<main onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<article onClick={() => {}} />;', errors: [expectedError] },
{ code: '<aside onClick={() => {}} />;', errors: [expectedError] },
{ code: '<blockquote onClick={() => {}} />;', errors: [expectedError] },
{ code: '<body onClick={() => {}} />;', errors: [expectedError] },
{ code: '<br onClick={() => {}} />;', errors: [expectedError] },
{ code: '<caption onClick={() => {}} />;', errors: [expectedError] },
{ code: '<dd onClick={() => {}} />;', errors: [expectedError] },
{ code: '<details onClick={() => {}} />;', errors: [expectedError] },
{ code: '<dfn onClick={() => {}} />;', errors: [expectedError] },
{ code: '<dl onClick={() => {}} />;', errors: [expectedError] },
{ code: '<dir onClick={() => {}} />;', errors: [expectedError] },
{ code: '<dt onClick={() => {}} />;', errors: [expectedError] },
{ code: '<fieldset onClick={() => {}} />;', errors: [expectedError] },
{ code: '<figcaption onClick={() => {}} />;', errors: [expectedError] },
{ code: '<figure onClick={() => {}} />;', errors: [expectedError] },
{ code: '<footer onClick={() => {}} />;', errors: [expectedError] },
{ code: '<form onClick={() => {}} />;', errors: [expectedError] },
{ code: '<frame onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h1 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h2 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h3 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h4 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h5 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h6 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<hr onClick={() => {}} />;', errors: [expectedError] },
{ code: '<iframe onClick={() => {}} />;', errors: [expectedError] },
{ code: '<img onClick={() => {}} />;', errors: [expectedError] },
{ code: '<label onClick={() => {}} />;', errors: [expectedError] },
{ code: '<legend onClick={() => {}} />;', errors: [expectedError] },
{ code: '<li onClick={() => {}} />;', errors: [expectedError] },
{ code: '<mark onClick={() => {}} />;', errors: [expectedError] },
{ code: '<marquee onClick={() => {}} />;', errors: [expectedError] },
{ code: '<menu onClick={() => {}} />;', errors: [expectedError] },
{ code: '<meter onClick={() => {}} />;', errors: [expectedError] },
{ code: '<nav onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ol onClick={() => {}} />;', errors: [expectedError] },
{ code: '<optgroup onClick={() => {}} />;', errors: [expectedError] },
{ code: '<output onClick={() => {}} />;', errors: [expectedError] },
{ code: '<p onClick={() => {}} />;', errors: [expectedError] },
{ code: '<pre onClick={() => {}} />;', errors: [expectedError] },
{ code: '<progress onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ruby onClick={() => {}} />;', errors: [expectedError] },
{ code: '<section onClick={() => {}} aria-label="Aardvark" />;', errors: [expectedError] },
{ code: '<section onClick={() => {}} aria-labelledby="js_1" />;', errors: [expectedError] },
{ code: '<table onClick={() => {}} />;', errors: [expectedError] },
{ code: '<tbody onClick={() => {}} />;', errors: [expectedError] },
{ code: '<td onClick={() => {}} />;', errors: [expectedError] },
{ code: '<tfoot onClick={() => {}} />;', errors: [expectedError] },
{ code: '<thead onClick={() => {}} />;', errors: [expectedError] },
{ code: '<time onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ul onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ul contentEditable="false" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<article contentEditable onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div contentEditable role="article" onKeyDown={() => {}} />;', errors: [expectedError] },
/* HTML elements attributed with a non-interactive role */
{ code: '<div role="alert" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="alertdialog" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="application" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="banner" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="cell" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="complementary" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="contentinfo" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="definition" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="dialog" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="directory" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="document" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="feed" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="figure" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="form" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="group" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="heading" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="img" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="list" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="listitem" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="log" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="main" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="marquee" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="math" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="navigation" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="note" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="region" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="rowgroup" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="search" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="separator" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="status" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="table" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="tabpanel" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="term" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="timer" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="tooltip" onClick={() => {}} />;', errors: [expectedError] },
// Handlers
{ code: '<div role="article" onKeyDown={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onKeyPress={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onKeyUp={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onLoad={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onError={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseDown={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseUp={() => {}} />;', errors: [expectedError] },
// Custom component
{
code: '<Image onClick={() => void 0} />;',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { Image: 'img' } } },
},
];
const recommendedOptions = configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {};
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
// All the possible handlers
{ code: '<div role="article" onCopy={() => {}} />;' },
{ code: '<div role="article" onCut={() => {}} />;' },
{ code: '<div role="article" onPaste={() => {}} />;' },
{ code: '<div role="article" onCompositionEnd={() => {}} />;' },
{ code: '<div role="article" onCompositionStart={() => {}} />;' },
{ code: '<div role="article" onCompositionUpdate={() => {}} />;' },
{ code: '<div role="article" onFocus={() => {}} />;' },
{ code: '<div role="article" onBlur={() => {}} />;' },
{ code: '<div role="article" onChange={() => {}} />;' },
{ code: '<div role="article" onInput={() => {}} />;' },
{ code: '<div role="article" onSubmit={() => {}} />;' },
{ code: '<div role="article" onContextMenu={() => {}} />;' },
{ code: '<div role="article" onDblClick={() => {}} />;' },
{ code: '<div role="article" onDoubleClick={() => {}} />;' },
{ code: '<div role="article" onDrag={() => {}} />;' },
{ code: '<div role="article" onDragEnd={() => {}} />;' },
{ code: '<div role="article" onDragEnter={() => {}} />;' },
{ code: '<div role="article" onDragExit={() => {}} />;' },
{ code: '<div role="article" onDragLeave={() => {}} />;' },
{ code: '<div role="article" onDragOver={() => {}} />;' },
{ code: '<div role="article" onDragStart={() => {}} />;' },
{ code: '<div role="article" onDrop={() => {}} />;' },
{ code: '<div role="article" onMouseEnter={() => {}} />;' },
{ code: '<div role="article" onMouseLeave={() => {}} />;' },
{ code: '<div role="article" onMouseMove={() => {}} />;' },
{ code: '<div role="article" onMouseOut={() => {}} />;' },
{ code: '<div role="article" onMouseOver={() => {}} />;' },
{ code: '<div role="article" onSelect={() => {}} />;' },
{ code: '<div role="article" onTouchCancel={() => {}} />;' },
{ code: '<div role="article" onTouchEnd={() => {}} />;' },
{ code: '<div role="article" onTouchMove={() => {}} />;' },
{ code: '<div role="article" onTouchStart={() => {}} />;' },
{ code: '<div role="article" onScroll={() => {}} />;' },
{ code: '<div role="article" onWheel={() => {}} />;' },
{ code: '<div role="article" onAbort={() => {}} />;' },
{ code: '<div role="article" onCanPlay={() => {}} />;' },
{ code: '<div role="article" onCanPlayThrough={() => {}} />;' },
{ code: '<div role="article" onDurationChange={() => {}} />;' },
{ code: '<div role="article" onEmptied={() => {}} />;' },
{ code: '<div role="article" onEncrypted={() => {}} />;' },
{ code: '<div role="article" onEnded={() => {}} />;' },
{ code: '<div role="article" onLoadedData={() => {}} />;' },
{ code: '<div role="article" onLoadedMetadata={() => {}} />;' },
{ code: '<div role="article" onLoadStart={() => {}} />;' },
{ code: '<div role="article" onPause={() => {}} />;' },
{ code: '<div role="article" onPlay={() => {}} />;' },
{ code: '<div role="article" onPlaying={() => {}} />;' },
{ code: '<div role="article" onProgress={() => {}} />;' },
{ code: '<div role="article" onRateChange={() => {}} />;' },
{ code: '<div role="article" onSeeked={() => {}} />;' },
{ code: '<div role="article" onSeeking={() => {}} />;' },
{ code: '<div role="article" onStalled={() => {}} />;' },
{ code: '<div role="article" onSuspend={() => {}} />;' },
{ code: '<div role="article" onTimeUpdate={() => {}} />;' },
{ code: '<div role="article" onVolumeChange={() => {}} />;' },
{ code: '<div role="article" onWaiting={() => {}} />;' },
{ code: '<div role="article" onAnimationStart={() => {}} />;' },
{ code: '<div role="article" onAnimationEnd={() => {}} />;' },
{ code: '<div role="article" onAnimationIteration={() => {}} />;' },
{ code: '<div role="article" onTransitionEnd={() => {}} />;' },
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
const strictOptions = configs.strict.rules[`jsx-a11y/${ruleName}`][1] || {};
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
))
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
// All the possible handlers
{ code: '<div role="article" onFocus={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onBlur={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onContextMenu={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDblClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDoubleClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDrag={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragEnd={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragEnter={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragExit={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragLeave={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragOver={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragStart={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDrop={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseEnter={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseLeave={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseMove={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseOut={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseOver={() => {}} />;', errors: [expectedError] },
))
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,500 @@
/**
* @fileoverview Disallow inherently non-interactive elements to be assigned
* interactive roles.
* @author Jesse Beach
* @author $AUTHOR
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-noninteractive-element-to-interactive-role';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = 'Non-interactive elements should not be assigned interactive roles.';
const expectedError = {
message: errorMessage,
type: 'JSXAttribute',
};
const ruleName = 'jsx-a11y/no-noninteractive-element-to-interactive-role';
const componentsSettings = {
'jsx-a11y': {
components: {
Article: 'article',
Input: 'input',
},
},
};
const alwaysValid = [
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
/* Interactive elements */
{ code: '<a tabIndex="0" role="button" />' },
{ code: '<a href="http://x.y.z" role="button" />' },
{ code: '<a href="http://x.y.z" tabIndex="0" role="button" />' },
{ code: '<area role="button" />;' },
{ code: '<area role="menuitem" />;' },
{ code: '<button className="foo" role="button" />' },
/* All flavors of input */
{ code: '<input role="button" />' },
{ code: '<input type="button" role="button" />' },
{ code: '<input type="checkbox" role="button" />' },
{ code: '<input type="color" role="button" />' },
{ code: '<input type="date" role="button" />' },
{ code: '<input type="datetime" role="button" />' },
{ code: '<input type="datetime-local" role="button" />' },
{ code: '<input type="email" role="button" />' },
{ code: '<input type="file" role="button" />' },
{ code: '<input type="hidden" role="button" />' },
{ code: '<input type="image" role="button" />' },
{ code: '<input type="month" role="button" />' },
{ code: '<input type="number" role="button" />' },
{ code: '<input type="password" role="button" />' },
{ code: '<input type="radio" role="button" />' },
{ code: '<input type="range" role="button" />' },
{ code: '<input type="reset" role="button" />' },
{ code: '<input type="search" role="button" />' },
{ code: '<input type="submit" role="button" />' },
{ code: '<input type="tel" role="button" />' },
{ code: '<input type="text" role="button" />' },
{ code: '<input type="time" role="button" />' },
{ code: '<input type="url" role="button" />' },
{ code: '<input type="week" role="button" />' },
{ code: '<input type="hidden" role="img" />' },
/* End all flavors of input */
{ code: '<menuitem role="button" />;' },
{ code: '<option className="foo" role="button" />' },
{ code: '<select className="foo" role="button" />' },
{ code: '<textarea className="foo" role="button" />' },
{ code: '<tr role="button" />;' },
{ code: '<tr role="presentation" />;' },
/* Interactive elements */
{ code: '<a tabIndex="0" role="img" />' },
{ code: '<a href="http://x.y.z" role="img" />' },
{ code: '<a href="http://x.y.z" tabIndex="0" role="img" />' },
/* All flavors of input */
{ code: '<input role="img" />' },
{ code: '<input type="img" role="img" />' },
{ code: '<input type="checkbox" role="img" />' },
{ code: '<input type="color" role="img" />' },
{ code: '<input type="date" role="img" />' },
{ code: '<input type="datetime" role="img" />' },
{ code: '<input type="datetime-local" role="img" />' },
{ code: '<input type="email" role="img" />' },
{ code: '<input type="file" role="img" />' },
{ code: '<input type="hidden" role="button" />' },
{ code: '<input type="image" role="img" />' },
{ code: '<input type="month" role="img" />' },
{ code: '<input type="number" role="img" />' },
{ code: '<input type="password" role="img" />' },
{ code: '<input type="radio" role="img" />' },
{ code: '<input type="range" role="img" />' },
{ code: '<input type="reset" role="img" />' },
{ code: '<input type="search" role="img" />' },
{ code: '<input type="submit" role="img" />' },
{ code: '<input type="tel" role="img" />' },
{ code: '<input type="text" role="img" />' },
{ code: '<input type="time" role="img" />' },
{ code: '<input type="url" role="img" />' },
{ code: '<input type="week" role="img" />' },
/* End all flavors of input */
{ code: '<menuitem role="img" />;' },
{ code: '<option className="foo" role="img" />' },
{ code: '<select className="foo" role="img" />' },
{ code: '<textarea className="foo" role="img" />' },
{ code: '<tr role="img" />;' },
/* Interactive elements */
{ code: '<a tabIndex="0" role="listitem" />' },
{ code: '<a href="http://x.y.z" role="listitem" />' },
{ code: '<a href="http://x.y.z" tabIndex="0" role="listitem" />' },
/* All flavors of input */
{ code: '<input role="listitem" />' },
{ code: '<input type="listitem" role="listitem" />' },
{ code: '<input type="checkbox" role="listitem" />' },
{ code: '<input type="color" role="listitem" />' },
{ code: '<input type="date" role="listitem" />' },
{ code: '<input type="datetime" role="listitem" />' },
{ code: '<input type="datetime-local" role="listitem" />' },
{ code: '<input type="email" role="listitem" />' },
{ code: '<input type="file" role="listitem" />' },
{ code: '<input type="image" role="listitem" />' },
{ code: '<input type="month" role="listitem" />' },
{ code: '<input type="number" role="listitem" />' },
{ code: '<input type="password" role="listitem" />' },
{ code: '<input type="radio" role="listitem" />' },
{ code: '<input type="range" role="listitem" />' },
{ code: '<input type="reset" role="listitem" />' },
{ code: '<input type="search" role="listitem" />' },
{ code: '<input type="submit" role="listitem" />' },
{ code: '<input type="tel" role="listitem" />' },
{ code: '<input type="text" role="listitem" />' },
{ code: '<input type="time" role="listitem" />' },
{ code: '<input type="url" role="listitem" />' },
{ code: '<input type="week" role="listitem" />' },
/* End all flavors of input */
{ code: '<menuitem role="listitem" />;' },
{ code: '<option className="foo" role="listitem" />' },
{ code: '<select className="foo" role="listitem" />' },
{ code: '<textarea className="foo" role="listitem" />' },
{ code: '<tr role="listitem" />;' },
/* HTML elements with neither an interactive or non-interactive valence (static) */
{ code: '<acronym role="button" />;' },
{ code: '<address role="button" />;' },
{ code: '<applet role="button" />;' },
{ code: '<audio role="button" />;' },
{ code: '<b role="button" />;' },
{ code: '<base role="button" />;' },
{ code: '<bdi role="button" />;' },
{ code: '<bdo role="button" />;' },
{ code: '<big role="button" />;' },
{ code: '<blink role="button" />;' },
{ code: '<canvas role="button" />;' },
{ code: '<center role="button" />;' },
{ code: '<cite role="button" />;' },
{ code: '<code role="button" />;' },
{ code: '<col role="button" />;' },
{ code: '<colgroup role="button" />;' },
{ code: '<content role="button" />;' },
{ code: '<data role="button" />;' },
{ code: '<datalist role="button" />;' },
{ code: '<del role="button" />;' },
{ code: '<div role="button" />;' },
{ code: '<div className="foo" role="button" />;' },
{ code: '<div className="foo" {...props} role="button" />;' },
{ code: '<div aria-hidden role="button" />;' },
{ code: '<div aria-hidden={true} role="button" />;' },
{ code: '<div role="button" />;' },
{ code: '<div role={undefined} role="button" />;' },
{ code: '<div {...props} role="button" />;' },
{ code: '<div onKeyUp={() => void 0} aria-hidden={false} role="button" />;' },
{ code: '<em role="button" />;' },
{ code: '<embed role="button" />;' },
{ code: '<font role="button" />;' },
{ code: '<frameset role="button" />;' },
{ code: '<head role="button" />;' },
{ code: '<header role="button" />;' },
{ code: '<hgroup role="button" />;' },
{ code: '<html role="button" />;' },
{ code: '<i role="button" />;' },
{ code: '<ins role="button" />;' },
{ code: '<kbd role="button" />;' },
{ code: '<keygen role="button" />;' },
{ code: '<link role="button" />;' },
{ code: '<map role="button" />;' },
{ code: '<meta role="button" />;' },
{ code: '<noembed role="button" />;' },
{ code: '<noscript role="button" />;' },
{ code: '<object role="button" />;' },
{ code: '<param role="button" />;' },
{ code: '<picture role="button" />;' },
{ code: '<q role="button" />;' },
{ code: '<rp role="button" />;' },
{ code: '<rt role="button" />;' },
{ code: '<rtc role="button" />;' },
{ code: '<s role="button" />;' },
{ code: '<samp role="button" />;' },
{ code: '<script role="button" />;' },
{ code: '<small role="button" />;' },
{ code: '<source role="button" />;' },
{ code: '<spacer role="button" />;' },
{ code: '<span role="button" />;' },
{ code: '<strike role="button" />;' },
{ code: '<strong role="button" />;' },
{ code: '<style role="button" />;' },
{ code: '<sub role="button" />;' },
{ code: '<summary role="button" />;' },
{ code: '<sup role="button" />;' },
{ code: '<th role="button" />;' },
{ code: '<title role="button" />;' },
{ code: '<track role="button" />;' },
{ code: '<tt role="button" />;' },
{ code: '<u role="button" />;' },
{ code: '<var role="button" />;' },
{ code: '<video role="button" />;' },
{ code: '<wbr role="button" />;' },
{ code: '<xmp role="button" />;' },
/* HTML elements attributed with an interactive role */
{ code: '<div role="button" />;' },
{ code: '<div role="checkbox" />;' },
{ code: '<div role="columnheader" />;' },
{ code: '<div role="combobox" />;' },
{ code: '<div role="grid" />;' },
{ code: '<div role="gridcell" />;' },
{ code: '<div role="link" />;' },
{ code: '<div role="listbox" />;' },
{ code: '<div role="menu" />;' },
{ code: '<div role="menubar" />;' },
{ code: '<div role="menuitem" />;' },
{ code: '<div role="menuitemcheckbox" />;' },
{ code: '<div role="menuitemradio" />;' },
{ code: '<div role="option" />;' },
{ code: '<div role="progressbar" />;' },
{ code: '<div role="radio" />;' },
{ code: '<div role="radiogroup" />;' },
{ code: '<div role="row" />;' },
{ code: '<div role="rowheader" />;' },
{ code: '<div role="searchbox" />;' },
{ code: '<div role="slider" />;' },
{ code: '<div role="spinbutton" />;' },
{ code: '<div role="switch" />;' },
{ code: '<div role="tab" />;' },
{ code: '<div role="textbox" />;' },
{ code: '<div role="treeitem" />;' },
/* Presentation is a special case role that indicates intentional static semantics */
{ code: '<div role="presentation" />;' },
/* HTML elements attributed with an abstract role */
{ code: '<div role="command" />;' },
{ code: '<div role="composite" />;' },
{ code: '<div role="input" />;' },
{ code: '<div role="landmark" />;' },
{ code: '<div role="range" />;' },
{ code: '<div role="roletype" />;' },
{ code: '<div role="section" />;' },
{ code: '<div role="sectionhead" />;' },
{ code: '<div role="select" />;' },
{ code: '<div role="structure" />;' },
{ code: '<div role="tablist" />;' },
{ code: '<div role="toolbar" />;' },
{ code: '<div role="tree" />;' },
{ code: '<div role="treegrid" />;' },
{ code: '<div role="widget" />;' },
{ code: '<div role="window" />;' },
/* HTML elements with an inherent non-interactive role, assigned an
* interactive role. */
{ code: '<main role="listitem" />;' },
{ code: '<a role="listitem" />' },
{ code: '<a role="listitem" />;' },
{ code: '<a role="button" />' },
{ code: '<a role="button" />;' },
{ code: '<a role="menuitem" />' },
{ code: '<a role="menuitem" />;' },
{ code: '<area role="listitem" />;' },
{ code: '<article role="listitem" />;' },
{ code: '<article role="listitem" />;' },
{ code: '<dd role="listitem" />;' },
{ code: '<dfn role="listitem" />;' },
{ code: '<dt role="listitem" />;' },
{ code: '<fieldset role="listitem" />;' },
{ code: '<figure role="listitem" />;' },
{ code: '<form role="listitem" />;' },
{ code: '<frame role="listitem" />;' },
{ code: '<h1 role="listitem" />;' },
{ code: '<h2 role="listitem" />;' },
{ code: '<h3 role="listitem" />;' },
{ code: '<h4 role="listitem" />;' },
{ code: '<h5 role="listitem" />;' },
{ code: '<h6 role="listitem" />;' },
{ code: '<hr role="listitem" />;' },
{ code: '<img role="listitem" />;' },
{ code: '<li role="listitem" />;' },
{ code: '<li role="presentation" />;' },
{ code: '<nav role="listitem" />;' },
{ code: '<ol role="listitem" />;' },
{ code: '<table role="listitem" />;' },
{ code: '<tbody role="listitem" />;' },
{ code: '<td role="listitem" />;' },
{ code: '<tfoot role="listitem" />;' },
{ code: '<thead role="listitem" />;' },
{ code: '<ul role="listitem" />;' },
/* HTML elements attributed with a non-interactive role */
{ code: '<div role="alert" />;' },
{ code: '<div role="alertdialog" />;' },
{ code: '<div role="application" />;' },
{ code: '<div role="article" />;' },
{ code: '<div role="banner" />;' },
{ code: '<div role="cell" />;' },
{ code: '<div role="complementary" />;' },
{ code: '<div role="contentinfo" />;' },
{ code: '<div role="definition" />;' },
{ code: '<div role="dialog" />;' },
{ code: '<div role="directory" />;' },
{ code: '<div role="document" />;' },
{ code: '<div role="feed" />;' },
{ code: '<div role="figure" />;' },
{ code: '<div role="form" />;' },
{ code: '<div role="group" />;' },
{ code: '<div role="heading" />;' },
{ code: '<div role="img" />;' },
{ code: '<div role="list" />;' },
{ code: '<div role="listitem" />;' },
{ code: '<div role="log" />;' },
{ code: '<div role="main" />;' },
{ code: '<div role="marquee" />;' },
{ code: '<div role="math" />;' },
{ code: '<div role="navigation" />;' },
{ code: '<div role="note" />;' },
{ code: '<div role="region" />;' },
{ code: '<div role="rowgroup" />;' },
{ code: '<div role="search" />;' },
{ code: '<div role="separator" />;' },
{ code: '<div role="scrollbar" />;' },
{ code: '<div role="status" />;' },
{ code: '<div role="table" />;' },
{ code: '<div role="tabpanel" />;' },
{ code: '<div role="term" />;' },
{ code: '<div role="timer" />;' },
{ code: '<div role="tooltip" />;' },
{ code: '<ul role="list" />;' },
/* Custom components */
{ code: '<Article role="button" />' },
{ code: '<Input role="button" />', settings: componentsSettings },
];
const neverValid = [
/* HTML elements with an inherent non-interactive role, assigned an
* interactive role. */
{ code: '<main role="button" />;', errors: [expectedError] },
{ code: '<article role="button" />;', errors: [expectedError] },
{ code: '<aside role="button" />;', errors: [expectedError] },
{ code: '<blockquote role="button" />;', errors: [expectedError] },
{ code: '<body role="button" />;', errors: [expectedError] },
{ code: '<br role="button" />;', errors: [expectedError] },
{ code: '<caption role="button" />;', errors: [expectedError] },
{ code: '<dd role="button" />;', errors: [expectedError] },
{ code: '<details role="button" />;', errors: [expectedError] },
{ code: '<dir role="button" />;', errors: [expectedError] },
{ code: '<dl role="button" />;', errors: [expectedError] },
{ code: '<dfn role="button" />;', errors: [expectedError] },
{ code: '<dt role="button" />;', errors: [expectedError] },
{ code: '<fieldset role="button" />;', errors: [expectedError] },
{ code: '<figcaption role="button" />;', errors: [expectedError] },
{ code: '<figure role="button" />;', errors: [expectedError] },
{ code: '<footer role="button" />;', errors: [expectedError] },
{ code: '<form role="button" />;', errors: [expectedError] },
{ code: '<frame role="button" />;', errors: [expectedError] },
{ code: '<h1 role="button" />;', errors: [expectedError] },
{ code: '<h2 role="button" />;', errors: [expectedError] },
{ code: '<h3 role="button" />;', errors: [expectedError] },
{ code: '<h4 role="button" />;', errors: [expectedError] },
{ code: '<h5 role="button" />;', errors: [expectedError] },
{ code: '<h6 role="button" />;', errors: [expectedError] },
{ code: '<hr role="button" />;', errors: [expectedError] },
{ code: '<iframe role="button" />;', errors: [expectedError] },
{ code: '<img role="button" />;', errors: [expectedError] },
{ code: '<label role="button" />;', errors: [expectedError] },
{ code: '<legend role="button" />;', errors: [expectedError] },
{ code: '<li role="button" />;', errors: [expectedError] },
{ code: '<mark role="button" />;', errors: [expectedError] },
{ code: '<marquee role="button" />;', errors: [expectedError] },
{ code: '<menu role="button" />;', errors: [expectedError] },
{ code: '<meter role="button" />;', errors: [expectedError] },
{ code: '<nav role="button" />;', errors: [expectedError] },
{ code: '<ol role="button" />;', errors: [expectedError] },
{ code: '<optgroup role="button" />;', errors: [expectedError] },
{ code: '<output role="button" />;', errors: [expectedError] },
{ code: '<pre role="button" />;', errors: [expectedError] },
{ code: '<progress role="button" />;', errors: [expectedError] },
{ code: '<ruby role="button" />;', errors: [expectedError] },
{ code: '<table role="button" />;', errors: [expectedError] },
{ code: '<tbody role="button" />;', errors: [expectedError] },
{ code: '<td role="button" />;', errors: [expectedError] },
{ code: '<tfoot role="button" />;', errors: [expectedError] },
{ code: '<thead role="button" />;', errors: [expectedError] },
{ code: '<time role="button" />;', errors: [expectedError] },
{ code: '<ul role="button" />;', errors: [expectedError] },
/* HTML elements with an inherent non-interactive role, assigned an
* interactive role. */
{ code: '<main role="menuitem" />;', errors: [expectedError] },
{ code: '<article role="menuitem" />;', errors: [expectedError] },
{ code: '<dd role="menuitem" />;', errors: [expectedError] },
{ code: '<dfn role="menuitem" />;', errors: [expectedError] },
{ code: '<dt role="menuitem" />;', errors: [expectedError] },
{ code: '<fieldset role="menuitem" />;', errors: [expectedError] },
{ code: '<figure role="menuitem" />;', errors: [expectedError] },
{ code: '<form role="menuitem" />;', errors: [expectedError] },
{ code: '<frame role="menuitem" />;', errors: [expectedError] },
{ code: '<h1 role="menuitem" />;', errors: [expectedError] },
{ code: '<h2 role="menuitem" />;', errors: [expectedError] },
{ code: '<h3 role="menuitem" />;', errors: [expectedError] },
{ code: '<h4 role="menuitem" />;', errors: [expectedError] },
{ code: '<h5 role="menuitem" />;', errors: [expectedError] },
{ code: '<h6 role="menuitem" />;', errors: [expectedError] },
{ code: '<hr role="menuitem" />;', errors: [expectedError] },
{ code: '<img role="menuitem" />;', errors: [expectedError] },
{ code: '<nav role="menuitem" />;', errors: [expectedError] },
{ code: '<ol role="menuitem" />;', errors: [expectedError] },
{ code: '<p role="button" />;', errors: [expectedError] },
{ code: '<section role="button" aria-label="Aardvark" />;', errors: [expectedError] },
{ code: '<table role="menuitem" />;', errors: [expectedError] },
{ code: '<tbody role="menuitem" />;', errors: [expectedError] },
{ code: '<td role="menuitem" />;', errors: [expectedError] },
{ code: '<tfoot role="menuitem" />;', errors: [expectedError] },
{ code: '<thead role="menuitem" />;', errors: [expectedError] },
/* Custom components */
{ code: '<Article role="button" />', errors: [expectedError], settings: componentsSettings },
];
const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
{ code: '<ul role="menu" />;' },
{ code: '<ul role="menubar" />;' },
{ code: '<ul role="radiogroup" />;' },
{ code: '<ul role="tablist" />;' },
{ code: '<ul role="tree" />;' },
{ code: '<ul role="treegrid" />;' },
{ code: '<ol role="menu" />;' },
{ code: '<ol role="menubar" />;' },
{ code: '<ol role="radiogroup" />;' },
{ code: '<ol role="tablist" />;' },
{ code: '<ol role="tree" />;' },
{ code: '<ol role="treegrid" />;' },
{ code: '<li role="tab" />;' },
{ code: '<li role="menuitem" />;' },
{ code: '<li role="menuitemcheckbox" />;' },
{ code: '<li role="menuitemradio" />;' },
{ code: '<li role="row" />;' },
{ code: '<li role="treeitem" />;' },
{ code: '<Component role="treeitem" />;' },
{ code: '<fieldset role="radiogroup" />;' },
{ code: '<fieldset role="presentation" />;' },
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
{ code: '<ul role="menu" />;', errors: [expectedError] },
{ code: '<ul role="menubar" />;', errors: [expectedError] },
{ code: '<ul role="radiogroup" />;', errors: [expectedError] },
{ code: '<ul role="tablist" />;', errors: [expectedError] },
{ code: '<ul role="tree" />;', errors: [expectedError] },
{ code: '<ul role="treegrid" />;', errors: [expectedError] },
{ code: '<ol role="menu" />;', errors: [expectedError] },
{ code: '<ol role="menubar" />;', errors: [expectedError] },
{ code: '<ol role="radiogroup" />;', errors: [expectedError] },
{ code: '<ol role="tablist" />;', errors: [expectedError] },
{ code: '<ol role="tree" />;', errors: [expectedError] },
{ code: '<ol role="treegrid" />;', errors: [expectedError] },
{ code: '<li role="tab" />;', errors: [expectedError] },
{ code: '<li role="menuitem" />;', errors: [expectedError] },
{ code: '<li role="row" />;', errors: [expectedError] },
{ code: '<li role="treeitem" />;', errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,123 @@
/**
* @fileoverview Disallow tabindex on static and noninteractive elements
* @author jessebeach
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-noninteractive-tabindex';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const ruleName = 'no-noninteractive-tabindex';
const expectedError = {
message: '`tabIndex` should only be declared on interactive elements.',
type: 'JSXAttribute',
};
const componentsSettings = {
'jsx-a11y': {
components: {
Article: 'article',
MyButton: 'button',
},
},
};
const alwaysValid = [
{ code: '<MyButton tabIndex={0} />' },
{ code: '<button />' },
{ code: '<button tabIndex="0" />' },
{ code: '<button tabIndex={0} />' },
{ code: '<div />' },
{ code: '<div tabIndex="-1" />' },
{ code: '<div role="button" tabIndex="0" />' },
{ code: '<div role="article" tabIndex="-1" />' },
{ code: '<article tabIndex="-1" />' },
{ code: '<Article tabIndex="-1" />', settings: componentsSettings },
{ code: '<MyButton tabIndex={0} />', settings: componentsSettings },
];
const neverValid = [
{ code: '<div tabIndex="0" />', errors: [expectedError] },
{ code: '<div role="article" tabIndex="0" />', errors: [expectedError] },
{ code: '<article tabIndex="0" />', errors: [expectedError] },
{ code: '<article tabIndex={0} />', errors: [expectedError] },
{ code: '<Article tabIndex={0} />', errors: [expectedError], settings: componentsSettings },
];
const recommendedOptions = (
configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {}
);
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
{ code: '<div role="tabpanel" tabIndex="0" />' },
// Expressions should pass in recommended mode
{ code: '<div role={ROLE_BUTTON} onClick={() => {}} tabIndex="0" />;' },
// Cases for allowExpressionValues set to true
{
code: '<div role={BUTTON} onClick={() => {}} tabIndex="0" />;',
options: [{ allowExpressionValues: true }],
},
// Specific case for ternary operator with literals on both side
{
code: '<div role={isButton ? "button" : "link"} onClick={() => {}} tabIndex="0" />;',
options: [{ allowExpressionValues: true }],
},
{
code: '<div role={isButton ? "button" : LINK} onClick={() => {}} tabIndex="0" />;',
options: [{ allowExpressionValues: true }],
errors: [expectedError],
},
{
code: '<div role={isButton ? BUTTON : LINK} onClick={() => {}} tabIndex="0"/>;',
options: [{ allowExpressionValues: true }],
errors: [expectedError],
},
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
{ code: '<div role="tabpanel" tabIndex="0" />', errors: [expectedError] },
// Expressions should fail in strict mode
{ code: '<div role={ROLE_BUTTON} onClick={() => {}} tabIndex="0" />;', errors: [expectedError] },
// Cases for allowExpressionValues set to false
{
code: '<div role={BUTTON} onClick={() => {}} tabIndex="0" />;',
options: [{ allowExpressionValues: false }],
errors: [expectedError],
},
// Specific case for ternary operator with literals on both side
{
code: '<div role={isButton ? "button" : "link"} onClick={() => {}} tabIndex="0" />;',
options: [{ allowExpressionValues: false }],
errors: [expectedError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,57 @@
/**
* @fileoverview Enforce usage of onBlur over onChange on select menus for accessibility.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-onchange';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'onBlur must be used instead of onchange, unless absolutely necessary and it causes no negative consequences for keyboard only or screen reader users.',
type: 'JSXOpeningElement',
};
const componentsSettings = {
'jsx-a11y': {
components: {
CustomOption: 'option',
Input: 'input',
},
},
};
ruleTester.run('no-onchange', rule, {
valid: parsers.all([].concat(
{ code: '<select onBlur={() => {}} />;' },
{ code: '<select onBlur={handleOnBlur} />;' },
{ code: '<option />;' },
{ code: '<option onBlur={() => {}} onChange={() => {}} />;' },
{ code: '<option {...props} />' },
{ code: '<input onChange={() => {}} />;' },
{ code: '<input onChange={handleOnChange} />;' },
{ code: '<input />;' },
{ code: '<input onChange={() => {}} onChange={() => {}} />;' },
{ code: '<input {...props} />' },
{ code: '<Input onChange={() => {}} />;', settings: componentsSettings },
{ code: '<CustomOption onChange={() => {}} />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<select onChange={() => {}} />;', errors: [expectedError] },
{ code: '<select onChange={handleOnChange} />;', errors: [expectedError] },
{ code: '<option onChange={() => {}} />', errors: [expectedError] },
{ code: '<option onChange={() => {}} {...props} />', errors: [expectedError] },
{ code: '<CustomOption onChange={() => {}} />;', errors: [expectedError], settings: componentsSettings },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,94 @@
/**
* @fileoverview Enforce explicit role property is not the
* same as implicit default role property on element.
* @author Ethan Cohen <@evcohen>
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-redundant-roles';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = (element, implicitRole) => ({
message: `The element ${element} has an implicit role of ${implicitRole}. Defining this explicitly is redundant and should be avoided.`,
type: 'JSXOpeningElement',
});
const ruleName = 'jsx-a11y/no-redundant-roles';
const componentsSettings = {
'jsx-a11y': {
components: {
Button: 'button',
},
},
};
const alwaysValid = [
{ code: '<div />;' },
{ code: '<button role="main" />' },
{ code: '<MyComponent role="button" />' },
{ code: '<button role={`${foo}button`} />' },
{ code: '<Button role={`${foo}button`} />', settings: componentsSettings },
];
const neverValid = [
{ code: '<button role="button" />', errors: [expectedError('button', 'button')] },
{ code: '<body role="DOCUMENT" />', errors: [expectedError('body', 'document')] },
{ code: '<Button role="button" />', settings: componentsSettings, errors: [expectedError('button', 'button')] },
];
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
{ code: '<nav role="navigation" />' },
))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
neverValid,
))
.map(parserOptionsMapper),
});
const noNavExceptionsOptions = { nav: [] };
const listException = { ul: ['list'], ol: ['list'] };
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
alwaysValid
.map(ruleOptionsMapperFactory(noNavExceptionsOptions)),
))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
{ code: '<nav role="navigation" />', errors: [expectedError('nav', 'navigation')] },
))
.map(ruleOptionsMapperFactory(noNavExceptionsOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:recommended (valid list role override)`, rule, {
valid: parsers.all([].concat(
{ code: '<ul role="list" />' },
{ code: '<ol role="list" />' },
{ code: '<dl role="list" />' },
))
.map(ruleOptionsMapperFactory(listException))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<ul role="list" />', errors: [expectedError('ul', 'list')] },
{ code: '<ol role="list" />', errors: [expectedError('ol', 'list')] },
))
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,501 @@
/**
* @fileoverview Enforce static elements have no interactive handlers.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-static-element-interactions';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = 'Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.';
const expectedError = {
message: errorMessage,
type: 'JSXOpeningElement',
};
const ruleName = 'no-static-element-interactions';
const componentsSettings = {
'jsx-a11y': {
components: {
Button: 'button',
TestComponent: 'div',
},
},
};
const alwaysValid = [
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />', settings: componentsSettings },
{ code: '<div />;' },
{ code: '<div className="foo" />;' },
{ code: '<div className="foo" {...props} />;' },
{ code: '<div onClick={() => void 0} aria-hidden />;' },
{ code: '<div onClick={() => void 0} aria-hidden={true} />;' },
{ code: '<div onClick={null} />;' },
/* All flavors of input */
{ code: '<input onClick={() => void 0} />' },
{ code: '<input type="button" onClick={() => void 0} />' },
{ code: '<input type="checkbox" onClick={() => void 0} />' },
{ code: '<input type="color" onClick={() => void 0} />' },
{ code: '<input type="date" onClick={() => void 0} />' },
{ code: '<input type="datetime" onClick={() => void 0} />' },
{ code: '<input type="datetime-local" onClick={() => void 0} />' },
{ code: '<input type="email" onClick={() => void 0} />' },
{ code: '<input type="file" onClick={() => void 0} />' },
{ code: '<input type="hidden" onClick={() => void 0} />' },
{ code: '<input type="image" onClick={() => void 0} />' },
{ code: '<input type="month" onClick={() => void 0} />' },
{ code: '<input type="number" onClick={() => void 0} />' },
{ code: '<input type="password" onClick={() => void 0} />' },
{ code: '<input type="radio" onClick={() => void 0} />' },
{ code: '<input type="range" onClick={() => void 0} />' },
{ code: '<input type="reset" onClick={() => void 0} />' },
{ code: '<input type="search" onClick={() => void 0} />' },
{ code: '<input type="submit" onClick={() => void 0} />' },
{ code: '<input type="tel" onClick={() => void 0} />' },
{ code: '<input type="text" onClick={() => void 0} />' },
{ code: '<input type="time" onClick={() => void 0} />' },
{ code: '<input type="url" onClick={() => void 0} />' },
{ code: '<input type="week" onClick={() => void 0} />' },
/* End all flavors of input */
{ code: '<button onClick={() => void 0} className="foo" />' },
{ code: '<datalist onClick={() => {}} />;' },
{ code: '<menuitem onClick={() => {}} />;' },
{ code: '<option onClick={() => void 0} className="foo" />' },
{ code: '<select onClick={() => void 0} className="foo" />' },
{ code: '<textarea onClick={() => void 0} className="foo" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
{ code: '<audio onClick={() => {}} />;' },
{ code: '<form onClick={() => {}} />;' },
{ code: '<form onSubmit={() => {}} />;' },
{ code: '<link onClick={() => {}} href="#" />;' },
/* HTML elements attributed with an interactive role */
{ code: '<div role="button" onClick={() => {}} />;' },
{ code: '<div role="checkbox" onClick={() => {}} />;' },
{ code: '<div role="columnheader" onClick={() => {}} />;' },
{ code: '<div role="combobox" onClick={() => {}} />;' },
{ code: '<div role="form" onClick={() => {}} />;' },
{ code: '<div role="gridcell" onClick={() => {}} />;' },
{ code: '<div role="link" onClick={() => {}} />;' },
{ code: '<div role="menuitem" onClick={() => {}} />;' },
{ code: '<div role="menuitemcheckbox" onClick={() => {}} />;' },
{ code: '<div role="menuitemradio" onClick={() => {}} />;' },
{ code: '<div role="option" onClick={() => {}} />;' },
{ code: '<div role="radio" onClick={() => {}} />;' },
{ code: '<div role="rowheader" onClick={() => {}} />;' },
{ code: '<div role="searchbox" onClick={() => {}} />;' },
{ code: '<div role="slider" onClick={() => {}} />;' },
{ code: '<div role="spinbutton" onClick={() => {}} />;' },
{ code: '<div role="switch" onClick={() => {}} />;' },
{ code: '<div role="tab" onClick={() => {}} />;' },
{ code: '<div role="textbox" onClick={() => {}} />;' },
{ code: '<div role="treeitem" onClick={() => {}} />;' },
/* Presentation is a special case role that indicates intentional static semantics */
{ code: '<div role="presentation" onClick={() => {}} />;' },
{ code: '<div role="presentation" onKeyDown={() => {}} />;' },
/* HTML elements with an inherent, non-interactive role */
{ code: '<article onClick={() => {}} />;' },
{ code: '<article onDblClick={() => void 0} />;' },
{ code: '<aside onClick={() => {}} />;' },
{ code: '<blockquote onClick={() => {}} />;' },
{ code: '<body onClick={() => {}} />;' },
{ code: '<br onClick={() => {}} />;' },
{ code: '<canvas onClick={() => {}} />;' },
{ code: '<caption onClick={() => {}} />;' },
{ code: '<details onClick={() => {}} />;' },
{ code: '<dd onClick={() => {}} />;' },
{ code: '<dfn onClick={() => {}} />;' },
{ code: '<dir onClick={() => {}} />;' },
{ code: '<dl onClick={() => {}} />;' },
{ code: '<dt onClick={() => {}} />;' },
{ code: '<embed onClick={() => {}} />;' },
{ code: '<fieldset onClick={() => {}} />;' },
{ code: '<figcaption onClick={() => {}} />;' },
{ code: '<figure onClick={() => {}} />;' },
{ code: '<footer onClick={() => {}} />;' },
{ code: '<frame onClick={() => {}} />;' },
{ code: '<h1 onClick={() => {}} />;' },
{ code: '<h2 onClick={() => {}} />;' },
{ code: '<h3 onClick={() => {}} />;' },
{ code: '<h4 onClick={() => {}} />;' },
{ code: '<h5 onClick={() => {}} />;' },
{ code: '<h6 onClick={() => {}} />;' },
{ code: '<hr onClick={() => {}} />;' },
{ code: '<iframe onClick={() => {}} />;' },
{ code: '<img onClick={() => {}} />;' },
{ code: '<label onClick={() => {}} />;' },
{ code: '<legend onClick={() => {}} />;' },
{ code: '<li onClick={() => {}} />;' },
{ code: '<main onClick={() => void 0} />;' },
{ code: '<mark onClick={() => {}} />;' },
{ code: '<marquee onClick={() => {}} />;' },
{ code: '<menu onClick={() => {}} />;' },
{ code: '<meter onClick={() => {}} />;' },
{ code: '<nav onClick={() => {}} />;' },
{ code: '<ol onClick={() => {}} />;' },
{ code: '<optgroup onClick={() => {}} />;' },
{ code: '<output onClick={() => {}} />;' },
{ code: '<p onClick={() => {}} />;' },
{ code: '<pre onClick={() => {}} />;' },
{ code: '<progress onClick={() => {}} />;' },
{ code: '<ruby onClick={() => {}} />;' },
{ code: '<section onClick={() => {}} aria-label="Aa" />;' },
{ code: '<section onClick={() => {}} aria-labelledby="js_1" />;' },
{ code: '<table onClick={() => {}} />;' },
{ code: '<tbody onClick={() => {}} />;' },
{ code: '<tfoot onClick={() => {}} />;' },
{ code: '<th onClick={() => {}} />;' },
{ code: '<thead onClick={() => {}} />;' },
{ code: '<time onClick={() => {}} />;' },
{ code: '<tr onClick={() => {}} />;' },
{ code: '<video onClick={() => {}} />;' },
{ code: '<ul onClick={() => {}} />;' },
/* HTML elements attributed with an abstract role */
{ code: '<div role="command" onClick={() => {}} />;' },
{ code: '<div role="composite" onClick={() => {}} />;' },
{ code: '<div role="input" onClick={() => {}} />;' },
{ code: '<div role="landmark" onClick={() => {}} />;' },
{ code: '<div role="range" onClick={() => {}} />;' },
{ code: '<div role="roletype" onClick={() => {}} />;' },
{ code: '<div role="sectionhead" onClick={() => {}} />;' },
{ code: '<div role="select" onClick={() => {}} />;' },
{ code: '<div role="structure" onClick={() => {}} />;' },
{ code: '<div role="widget" onClick={() => {}} />;' },
{ code: '<div role="window" onClick={() => {}} />;' },
/* HTML elements attributed with a non-interactive role */
{ code: '<div role="alert" onClick={() => {}} />;' },
{ code: '<div role="alertdialog" onClick={() => {}} />;' },
{ code: '<div role="application" onClick={() => {}} />;' },
{ code: '<div role="article" onClick={() => {}} />;' },
{ code: '<div role="banner" onClick={() => {}} />;' },
{ code: '<div role="cell" onClick={() => {}} />;' },
{ code: '<div role="complementary" onClick={() => {}} />;' },
{ code: '<div role="contentinfo" onClick={() => {}} />;' },
{ code: '<div role="definition" onClick={() => {}} />;' },
{ code: '<div role="dialog" onClick={() => {}} />;' },
{ code: '<div role="directory" onClick={() => {}} />;' },
{ code: '<div role="document" onClick={() => {}} />;' },
{ code: '<div role="feed" onClick={() => {}} />;' },
{ code: '<div role="figure" onClick={() => {}} />;' },
{ code: '<div role="grid" onClick={() => {}} />;' },
{ code: '<div role="group" onClick={() => {}} />;' },
{ code: '<div role="heading" onClick={() => {}} />;' },
{ code: '<div role="img" onClick={() => {}} />;' },
{ code: '<div role="list" onClick={() => {}} />;' },
{ code: '<div role="listbox" onClick={() => {}} />;' },
{ code: '<div role="listitem" onClick={() => {}} />;' },
{ code: '<div role="log" onClick={() => {}} />;' },
{ code: '<div role="main" onClick={() => {}} />;' },
{ code: '<div role="marquee" onClick={() => {}} />;' },
{ code: '<div role="math" onClick={() => {}} />;' },
{ code: '<div role="menu" onClick={() => {}} />;' },
{ code: '<div role="menubar" onClick={() => {}} />;' },
{ code: '<div role="navigation" onClick={() => {}} />;' },
{ code: '<div role="note" onClick={() => {}} />;' },
{ code: '<div role="progressbar" onClick={() => {}} />;' },
{ code: '<div role="radiogroup" onClick={() => {}} />;' },
{ code: '<div role="region" onClick={() => {}} />;' },
{ code: '<div role="row" onClick={() => {}} />;' },
{ code: '<div role="rowgroup" onClick={() => {}} />;' },
{ code: '<div role="section" onClick={() => {}} />;' },
{ code: '<div role="search" onClick={() => {}} />;' },
{ code: '<div role="separator" onClick={() => {}} />;' },
{ code: '<div role="scrollbar" onClick={() => {}} />;' },
{ code: '<div role="status" onClick={() => {}} />;' },
{ code: '<div role="table" onClick={() => {}} />;' },
{ code: '<div role="tablist" onClick={() => {}} />;' },
{ code: '<div role="tabpanel" onClick={() => {}} />;' },
{ code: '<td onClick={() => {}} />;' },
{ code: '<div role="term" onClick={() => {}} />;' },
{ code: '<div role="timer" onClick={() => {}} />;' },
{ code: '<div role="toolbar" onClick={() => {}} />;' },
{ code: '<div role="tooltip" onClick={() => {}} />;' },
{ code: '<div role="tree" onClick={() => {}} />;' },
{ code: '<div role="treegrid" onClick={() => {}} />;' },
// All the possible handlers
{ code: '<div onCopy={() => {}} />;' },
{ code: '<div onCut={() => {}} />;' },
{ code: '<div onPaste={() => {}} />;' },
{ code: '<div onCompositionEnd={() => {}} />;' },
{ code: '<div onCompositionStart={() => {}} />;' },
{ code: '<div onCompositionUpdate={() => {}} />;' },
{ code: '<div onChange={() => {}} />;' },
{ code: '<div onInput={() => {}} />;' },
{ code: '<div onSubmit={() => {}} />;' },
{ code: '<div onSelect={() => {}} />;' },
{ code: '<div onTouchCancel={() => {}} />;' },
{ code: '<div onTouchEnd={() => {}} />;' },
{ code: '<div onTouchMove={() => {}} />;' },
{ code: '<div onTouchStart={() => {}} />;' },
{ code: '<div onScroll={() => {}} />;' },
{ code: '<div onWheel={() => {}} />;' },
{ code: '<div onAbort={() => {}} />;' },
{ code: '<div onCanPlay={() => {}} />;' },
{ code: '<div onCanPlayThrough={() => {}} />;' },
{ code: '<div onDurationChange={() => {}} />;' },
{ code: '<div onEmptied={() => {}} />;' },
{ code: '<div onEncrypted={() => {}} />;' },
{ code: '<div onEnded={() => {}} />;' },
{ code: '<div onError={() => {}} />;' },
{ code: '<div onLoadedData={() => {}} />;' },
{ code: '<div onLoadedMetadata={() => {}} />;' },
{ code: '<div onLoadStart={() => {}} />;' },
{ code: '<div onPause={() => {}} />;' },
{ code: '<div onPlay={() => {}} />;' },
{ code: '<div onPlaying={() => {}} />;' },
{ code: '<div onProgress={() => {}} />;' },
{ code: '<div onRateChange={() => {}} />;' },
{ code: '<div onSeeked={() => {}} />;' },
{ code: '<div onSeeking={() => {}} />;' },
{ code: '<div onStalled={() => {}} />;' },
{ code: '<div onSuspend={() => {}} />;' },
{ code: '<div onTimeUpdate={() => {}} />;' },
{ code: '<div onVolumeChange={() => {}} />;' },
{ code: '<div onWaiting={() => {}} />;' },
{ code: '<div onLoad={() => {}} />;' },
{ code: '<div onError={() => {}} />;' },
{ code: '<div onAnimationStart={() => {}} />;' },
{ code: '<div onAnimationEnd={() => {}} />;' },
{ code: '<div onAnimationIteration={() => {}} />;' },
{ code: '<div onTransitionEnd={() => {}} />;' },
];
const neverValid = [
{ code: '<div onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<div onClick={() => void 0} role={undefined} />;', errors: [expectedError] },
{ code: '<div onClick={() => void 0} {...props} />;', errors: [expectedError] },
{ code: '<div onKeyUp={() => void 0} aria-hidden={false} />;', errors: [expectedError] },
/* Static elements; no inherent role */
{ code: '<a onClick={() => void 0} />', errors: [expectedError] },
{ code: '<a onClick={() => {}} />;', errors: [expectedError] },
{ code: '<a tabIndex="0" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<area onClick={() => {}} />;', errors: [expectedError] },
{ code: '<acronym onClick={() => {}} />;', errors: [expectedError] },
{ code: '<address onClick={() => {}} />;', errors: [expectedError] },
{ code: '<applet onClick={() => {}} />;', errors: [expectedError] },
{ code: '<b onClick={() => {}} />;', errors: [expectedError] },
{ code: '<base onClick={() => {}} />;', errors: [expectedError] },
{ code: '<bdi onClick={() => {}} />;', errors: [expectedError] },
{ code: '<bdo onClick={() => {}} />;', errors: [expectedError] },
{ code: '<big onClick={() => {}} />;', errors: [expectedError] },
{ code: '<blink onClick={() => {}} />;', errors: [expectedError] },
{ code: '<center onClick={() => {}} />;', errors: [expectedError] },
{ code: '<cite onClick={() => {}} />;', errors: [expectedError] },
{ code: '<code onClick={() => {}} />;', errors: [expectedError] },
{ code: '<col onClick={() => {}} />;', errors: [expectedError] },
{ code: '<colgroup onClick={() => {}} />;', errors: [expectedError] },
{ code: '<content onClick={() => {}} />;', errors: [expectedError] },
{ code: '<data onClick={() => {}} />;', errors: [expectedError] },
{ code: '<del onClick={() => {}} />;', errors: [expectedError] },
{ code: '<em onClick={() => {}} />;', errors: [expectedError] },
{ code: '<font onClick={() => {}} />;', errors: [expectedError] },
{ code: '<frameset onClick={() => {}} />;', errors: [expectedError] },
{ code: '<head onClick={() => {}} />;', errors: [expectedError] },
{ code: '<header onClick={() => {}} />;', errors: [expectedError] },
{ code: '<hgroup onClick={() => {}} />;', errors: [expectedError] },
{ code: '<html onClick={() => {}} />;', errors: [expectedError] },
{ code: '<i onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ins onClick={() => {}} />;', errors: [expectedError] },
{ code: '<kbd onClick={() => {}} />;', errors: [expectedError] },
{ code: '<keygen onClick={() => {}} />;', errors: [expectedError] },
{ code: '<map onClick={() => {}} />;', errors: [expectedError] },
{ code: '<meta onClick={() => {}} />;', errors: [expectedError] },
{ code: '<noembed onClick={() => {}} />;', errors: [expectedError] },
{ code: '<noscript onClick={() => {}} />;', errors: [expectedError] },
{ code: '<object onClick={() => {}} />;', errors: [expectedError] },
{ code: '<param onClick={() => {}} />;', errors: [expectedError] },
{ code: '<picture onClick={() => {}} />;', errors: [expectedError] },
{ code: '<q onClick={() => {}} />;', errors: [expectedError] },
{ code: '<rp onClick={() => {}} />;', errors: [expectedError] },
{ code: '<rt onClick={() => {}} />;', errors: [expectedError] },
{ code: '<rtc onClick={() => {}} />;', errors: [expectedError] },
{ code: '<s onClick={() => {}} />;', errors: [expectedError] },
{ code: '<samp onClick={() => {}} />;', errors: [expectedError] },
{ code: '<script onClick={() => {}} />;', errors: [expectedError] },
{ code: '<section onClick={() => {}} />;', errors: [expectedError] },
{ code: '<small onClick={() => {}} />;', errors: [expectedError] },
{ code: '<source onClick={() => {}} />;', errors: [expectedError] },
{ code: '<spacer onClick={() => {}} />;', errors: [expectedError] },
{ code: '<span onClick={() => {}} />;', errors: [expectedError] },
{ code: '<strike onClick={() => {}} />;', errors: [expectedError] },
{ code: '<strong onClick={() => {}} />;', errors: [expectedError] },
{ code: '<style onClick={() => {}} />;', errors: [expectedError] },
{ code: '<sub onClick={() => {}} />;', errors: [expectedError] },
{ code: '<sup onClick={() => {}} />;', errors: [expectedError] },
{ code: '<summary onClick={() => {}} />;', errors: [expectedError] },
{ code: '<title onClick={() => {}} />;', errors: [expectedError] },
{ code: '<track onClick={() => {}} />;', errors: [expectedError] },
{ code: '<tt onClick={() => {}} />;', errors: [expectedError] },
{ code: '<u onClick={() => {}} />;', errors: [expectedError] },
{ code: '<var onClick={() => {}} />;', errors: [expectedError] },
{ code: '<wbr onClick={() => {}} />;', errors: [expectedError] },
{ code: '<xmp onClick={() => {}} />;', errors: [expectedError] },
// Handlers
{ code: '<div onKeyDown={() => {}} />;', errors: [expectedError] },
{ code: '<div onKeyPress={() => {}} />;', errors: [expectedError] },
{ code: '<div onKeyUp={() => {}} />;', errors: [expectedError] },
{ code: '<div onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseDown={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseUp={() => {}} />;', errors: [expectedError] },
// Custom components
{ code: '<TestComponent onClick={doFoo} />', settings: componentsSettings, errors: [expectedError] },
];
const recommendedOptions = configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {};
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
alwaysValid,
// All the possible handlers
{ code: '<div onCopy={() => {}} />;' },
{ code: '<div onCut={() => {}} />;' },
{ code: '<div onPaste={() => {}} />;' },
{ code: '<div onCompositionEnd={() => {}} />;' },
{ code: '<div onCompositionStart={() => {}} />;' },
{ code: '<div onCompositionUpdate={() => {}} />;' },
{ code: '<div onFocus={() => {}} />;' },
{ code: '<div onBlur={() => {}} />;' },
{ code: '<div onChange={() => {}} />;' },
{ code: '<div onInput={() => {}} />;' },
{ code: '<div onSubmit={() => {}} />;' },
{ code: '<div onContextMenu={() => {}} />;' },
{ code: '<div onDblClick={() => {}} />;' },
{ code: '<div onDoubleClick={() => {}} />;' },
{ code: '<div onDrag={() => {}} />;' },
{ code: '<div onDragEnd={() => {}} />;' },
{ code: '<div onDragEnter={() => {}} />;' },
{ code: '<div onDragExit={() => {}} />;' },
{ code: '<div onDragLeave={() => {}} />;' },
{ code: '<div onDragOver={() => {}} />;' },
{ code: '<div onDragStart={() => {}} />;' },
{ code: '<div onDrop={() => {}} />;' },
{ code: '<div onMouseEnter={() => {}} />;' },
{ code: '<div onMouseLeave={() => {}} />;' },
{ code: '<div onMouseMove={() => {}} />;' },
{ code: '<div onMouseOut={() => {}} />;' },
{ code: '<div onMouseOver={() => {}} />;' },
{ code: '<div onSelect={() => {}} />;' },
{ code: '<div onTouchCancel={() => {}} />;' },
{ code: '<div onTouchEnd={() => {}} />;' },
{ code: '<div onTouchMove={() => {}} />;' },
{ code: '<div onTouchStart={() => {}} />;' },
{ code: '<div onScroll={() => {}} />;' },
{ code: '<div onWheel={() => {}} />;' },
{ code: '<div onAbort={() => {}} />;' },
{ code: '<div onCanPlay={() => {}} />;' },
{ code: '<div onCanPlayThrough={() => {}} />;' },
{ code: '<div onDurationChange={() => {}} />;' },
{ code: '<div onEmptied={() => {}} />;' },
{ code: '<div onEncrypted={() => {}} />;' },
{ code: '<div onEnded={() => {}} />;' },
{ code: '<div onError={() => {}} />;' },
{ code: '<div onLoadedData={() => {}} />;' },
{ code: '<div onLoadedMetadata={() => {}} />;' },
{ code: '<div onLoadStart={() => {}} />;' },
{ code: '<div onPause={() => {}} />;' },
{ code: '<div onPlay={() => {}} />;' },
{ code: '<div onPlaying={() => {}} />;' },
{ code: '<div onProgress={() => {}} />;' },
{ code: '<div onRateChange={() => {}} />;' },
{ code: '<div onSeeked={() => {}} />;' },
{ code: '<div onSeeking={() => {}} />;' },
{ code: '<div onStalled={() => {}} />;' },
{ code: '<div onSuspend={() => {}} />;' },
{ code: '<div onTimeUpdate={() => {}} />;' },
{ code: '<div onVolumeChange={() => {}} />;' },
{ code: '<div onWaiting={() => {}} />;' },
{ code: '<div onLoad={() => {}} />;' },
{ code: '<div onError={() => {}} />;' },
{ code: '<div onAnimationStart={() => {}} />;' },
{ code: '<div onAnimationEnd={() => {}} />;' },
{ code: '<div onAnimationIteration={() => {}} />;' },
{ code: '<div onTransitionEnd={() => {}} />;' },
// Expressions should pass in recommended mode
{ code: '<div role={ROLE_BUTTON} onClick={() => {}} />;' },
{ code: '<div {...this.props} role={this.props.role} onKeyPress={e => this.handleKeyPress(e)}>{this.props.children}</div>' },
// Cases for allowExpressionValues set to true
{
code: '<div role={BUTTON} onClick={() => {}} />;',
options: [{ allowExpressionValues: true }],
},
// Specific case for ternary operator with literals on both side
{
code: '<div role={isButton ? "button" : "link"} onClick={() => {}} />;',
options: [{ allowExpressionValues: true }],
},
{
code: '<div role={isButton ? "button" : LINK} onClick={() => {}} />;',
options: [{ allowExpressionValues: true }],
errors: [expectedError],
},
{
code: '<div role={isButton ? BUTTON : LINK} onClick={() => {}} />;',
options: [{ allowExpressionValues: true }],
errors: [expectedError],
},
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
alwaysValid,
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
neverValid,
// All the possible handlers
{ code: '<div onContextMenu={() => {}} />;', errors: [expectedError] },
{ code: '<div onDblClick={() => {}} />;', errors: [expectedError] },
{ code: '<div onDoubleClick={() => {}} />;', errors: [expectedError] },
{ code: '<div onDrag={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragEnd={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragEnter={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragExit={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragLeave={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragOver={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragStart={() => {}} />;', errors: [expectedError] },
{ code: '<div onDrop={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseEnter={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseLeave={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseMove={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseOut={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseOver={() => {}} />;', errors: [expectedError] },
// Expressions should fail in strict mode
{ code: '<div role={ROLE_BUTTON} onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div {...this.props} role={this.props.role} onKeyPress={e => this.handleKeyPress(e)}>{this.props.children}</div>', errors: [expectedError] },
// Cases for allowExpressionValues set to false
{
code: '<div role={BUTTON} onClick={() => {}} />;',
options: [{ allowExpressionValues: false }],
errors: [expectedError],
},
// Specific case for ternary operator with literals on both side
{
code: '<div role={isButton ? "button" : "link"} onClick={() => {}} />;',
options: [{ allowExpressionValues: false }],
errors: [expectedError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,63 @@
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/prefer-tag-over-role';
const ruleTester = new RuleTester();
const expectedError = (role, tag) => ({
message: `Use ${tag} instead of the "${role}" role to ensure accessibility across all devices.`,
type: 'JSXOpeningElement',
});
ruleTester.run('prefer-tag-over-role', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<div role="unknown" />;' },
{ code: '<div role="also unknown" />;' },
{ code: '<other />' },
{ code: '<img role="img" />' },
{ code: '<input role="checkbox" />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{
code: '<div role="checkbox" />',
errors: [expectedError('checkbox', '<input type="checkbox">')],
},
{
code: '<div role="button checkbox" />',
errors: [expectedError('checkbox', '<input type="checkbox">')],
},
{
code: '<div role="heading" />',
errors: [
expectedError('heading', '<h1>, <h2>, <h3>, <h4>, <h5>, or <h6>'),
],
},
{
code: '<div role="link" />',
errors: [
expectedError(
'link',
'<a href=...>, <area href=...>, or <link href=...>',
),
],
},
{
code: '<div role="rowgroup" />',
errors: [expectedError('rowgroup', '<tbody>, <tfoot>, or <thead>')],
},
{
code: '<span role="checkbox" />',
errors: [expectedError('checkbox', '<input type="checkbox">')],
},
{
code: '<other role="checkbox" />',
errors: [expectedError('checkbox', '<input type="checkbox">')],
},
{
code: '<div role="banner" />',
errors: [expectedError('banner', '<header>')],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,135 @@
/**
* @fileoverview Enforce that elements with ARIA roles must
* have all required attributes for that role.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { roles } from 'aria-query';
import { RuleTester } from 'eslint';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import map from 'es-iterator-helpers/Iterator.prototype.map';
import toArray from 'es-iterator-helpers/Iterator.prototype.toArray';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/role-has-required-aria-props';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = (role) => {
const requiredProps = Object.keys(roles.get(role).requiredProps);
return {
message: `Elements with the ARIA role "${role}" must have the following attributes defined: ${requiredProps}`,
type: 'JSXAttribute',
};
};
const componentsSettings = {
'jsx-a11y': {
components: {
MyComponent: 'div',
},
},
};
// Create basic test cases using all valid role types.
const basicValidityTests = toArray(map(iterFrom(roles.keys()), (role) => {
const {
requiredProps: requiredPropKeyValues,
} = roles.get(role);
const requiredProps = Object.keys(requiredPropKeyValues);
const propChain = requiredProps.join(' ');
return {
code: `<div role="${role.toLowerCase()}" ${propChain} />`,
};
}));
ruleTester.run('role-has-required-aria-props', rule, {
valid: parsers.all([].concat(
{ code: '<Bar baz />' },
{ code: '<MyComponent role="combobox" />' },
// Variables should pass, as we are only testing literals.
{ code: '<div />' },
{ code: '<div></div>' },
{ code: '<div role={role} />' },
{ code: '<div role={role || "button"} />' },
{ code: '<div role={role || "foobar"} />' },
{ code: '<div role="row" />' },
{ code: '<span role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0"></span>' },
{ code: '<input role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0" {...props} type="checkbox" />' },
{ code: '<input type="checkbox" role="switch" />' },
{ code: '<MyComponent role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0" />', settings: componentsSettings },
)).concat(basicValidityTests).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// SLIDER
{ code: '<div role="slider" />', errors: [errorMessage('slider')] },
{
code: '<div role="slider" aria-valuemax />',
errors: [errorMessage('slider')],
},
{
code: '<div role="slider" aria-valuemax aria-valuemin />',
errors: [errorMessage('slider')],
},
// CHECKBOX
{ code: '<div role="checkbox" />', errors: [errorMessage('checkbox')] },
{ code: '<div role="checkbox" checked />', errors: [errorMessage('checkbox')] },
{
code: '<div role="checkbox" aria-chcked />',
errors: [errorMessage('checkbox')],
},
{
code: '<span role="checkbox" aria-labelledby="foo" tabindex="0"></span>',
errors: [errorMessage('checkbox')],
},
// COMBOBOX
{ code: '<div role="combobox" />', errors: [errorMessage('combobox')] },
{ code: '<div role="combobox" expanded />', errors: [errorMessage('combobox')] },
{
code: '<div role="combobox" aria-expandd />',
errors: [errorMessage('combobox')],
},
// SCROLLBAR
{ code: '<div role="scrollbar" />', errors: [errorMessage('scrollbar')] },
{
code: '<div role="scrollbar" aria-valuemax />',
errors: [errorMessage('scrollbar')],
},
{
code: '<div role="scrollbar" aria-valuemax aria-valuemin />',
errors: [errorMessage('scrollbar')],
},
{
code: '<div role="scrollbar" aria-valuemax aria-valuenow />',
errors: [errorMessage('scrollbar')],
},
{
code: '<div role="scrollbar" aria-valuemin aria-valuenow />',
errors: [errorMessage('scrollbar')],
},
{
code: '<div role="heading" />',
errors: [errorMessage('heading')],
},
{
code: '<div role="option" />',
errors: [errorMessage('option')],
},
// Custom element
{ code: '<MyComponent role="combobox" />', settings: componentsSettings, errors: [errorMessage('combobox')] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,575 @@
/**
* @fileoverview Enforce that an element does not have an unsupported ARIA attribute.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import {
aria,
roles,
} from 'aria-query';
import { RuleTester } from 'eslint';
import { version as eslintVersion } from 'eslint/package.json';
import semver from 'semver';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';
import map from 'es-iterator-helpers/Iterator.prototype.map';
import toArray from 'es-iterator-helpers/Iterator.prototype.toArray';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/role-supports-aria-props';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const generateErrorMessage = (attr, role, tag, isImplicit) => {
if (isImplicit) {
return `The attribute ${attr} is not supported by the role ${role}. This role is implicit on the element ${tag}.`;
}
return `The attribute ${attr} is not supported by the role ${role}.`;
};
const errorMessage = (attr, role, tag, isImplicit) => ({
message: generateErrorMessage(attr, role, tag, isImplicit),
type: 'JSXOpeningElement',
});
const componentsSettings = {
'jsx-a11y': {
components: {
Link: 'a',
},
},
};
const nonAbstractRoles = toArray(filter(iterFrom(roles.keys()), (role) => roles.get(role).abstract === false));
const createTests = (rolesNames) => rolesNames.reduce((tests, role) => {
const {
props: propKeyValues,
} = roles.get(role);
const validPropsForRole = Object.keys(propKeyValues);
const invalidPropsForRole = filter(
map(iterFrom(aria.keys()), (attribute) => attribute.toLowerCase()),
(attribute) => validPropsForRole.indexOf(attribute) === -1,
);
const normalRole = role.toLowerCase();
return [
tests[0].concat(validPropsForRole.map((prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
}))),
tests[1].concat(toArray(map(invalidPropsForRole, (prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
errors: [errorMessage(prop.toLowerCase(), normalRole, 'div', false)],
})))),
];
}, [[], []]);
const [validTests, invalidTests] = createTests(nonAbstractRoles);
ruleTester.run('role-supports-aria-props', rule, {
valid: parsers.all([].concat(
{ code: '<Foo bar />' },
{ code: '<div />' },
{ code: '<div id="main" />' },
{ code: '<div role />' },
{ code: '<div role="presentation" {...props} />' },
{ code: '<Foo.Bar baz={true} />' },
{ code: '<Link href="#" aria-checked />' },
// IMPLICIT ROLE TESTS
// A TESTS - implicit role is `link`
{ code: '<a href="#" aria-expanded />' },
{ code: '<a href="#" aria-atomic />' },
{ code: '<a href="#" aria-busy />' },
{ code: '<a href="#" aria-controls />' },
{ code: '<a href="#" aria-current />' },
{ code: '<a href="#" aria-describedby />' },
{ code: '<a href="#" aria-disabled />' },
{ code: '<a href="#" aria-dropeffect />' },
{ code: '<a href="#" aria-flowto />' },
{ code: '<a href="#" aria-haspopup />' },
{ code: '<a href="#" aria-grabbed />' },
{ code: '<a href="#" aria-hidden />' },
{ code: '<a href="#" aria-label />' },
{ code: '<a href="#" aria-labelledby />' },
{ code: '<a href="#" aria-live />' },
{ code: '<a href="#" aria-owns />' },
{ code: '<a href="#" aria-relevant />' },
// this will have global
{ code: '<a aria-checked />' },
// AREA TESTS - implicit role is `link`
{ code: '<area href="#" aria-expanded />' },
{ code: '<area href="#" aria-atomic />' },
{ code: '<area href="#" aria-busy />' },
{ code: '<area href="#" aria-controls />' },
{ code: '<area href="#" aria-describedby />' },
{ code: '<area href="#" aria-disabled />' },
{ code: '<area href="#" aria-dropeffect />' },
{ code: '<area href="#" aria-flowto />' },
{ code: '<area href="#" aria-grabbed />' },
{ code: '<area href="#" aria-haspopup />' },
{ code: '<area href="#" aria-hidden />' },
{ code: '<area href="#" aria-label />' },
{ code: '<area href="#" aria-labelledby />' },
{ code: '<area href="#" aria-live />' },
{ code: '<area href="#" aria-owns />' },
{ code: '<area href="#" aria-relevant />' },
// this will have global
{ code: '<area aria-checked />' },
// LINK TESTS - implicit role is `link`
{ code: '<link href="#" aria-expanded />' },
{ code: '<link href="#" aria-atomic />' },
{ code: '<link href="#" aria-busy />' },
{ code: '<link href="#" aria-controls />' },
{ code: '<link href="#" aria-describedby />' },
{ code: '<link href="#" aria-disabled />' },
{ code: '<link href="#" aria-dropeffect />' },
{ code: '<link href="#" aria-flowto />' },
{ code: '<link href="#" aria-grabbed />' },
{ code: '<link href="#" aria-hidden />' },
{ code: '<link href="#" aria-haspopup />' },
{ code: '<link href="#" aria-label />' },
{ code: '<link href="#" aria-labelledby />' },
{ code: '<link href="#" aria-live />' },
{ code: '<link href="#" aria-owns />' },
{ code: '<link href="#" aria-relevant />' },
// this will have global
{ code: '<link aria-checked />' },
// IMG TESTS - no implicit role
{ code: '<img alt="" aria-checked />' },
// this will have role of `img`
{ code: '<img alt="foobar" aria-busy />' },
// MENU TESTS - implicit role is `toolbar` when `type="toolbar"`
{ code: '<menu type="toolbar" aria-activedescendant />' },
{ code: '<menu type="toolbar" aria-atomic />' },
{ code: '<menu type="toolbar" aria-busy />' },
{ code: '<menu type="toolbar" aria-controls />' },
{ code: '<menu type="toolbar" aria-describedby />' },
{ code: '<menu type="toolbar" aria-disabled />' },
{ code: '<menu type="toolbar" aria-dropeffect />' },
{ code: '<menu type="toolbar" aria-flowto />' },
{ code: '<menu type="toolbar" aria-grabbed />' },
{ code: '<menu type="toolbar" aria-hidden />' },
{ code: '<menu type="toolbar" aria-label />' },
{ code: '<menu type="toolbar" aria-labelledby />' },
{ code: '<menu type="toolbar" aria-live />' },
{ code: '<menu type="toolbar" aria-owns />' },
{ code: '<menu type="toolbar" aria-relevant />' },
// this will have global
{ code: '<menu aria-checked />' },
// MENUITEM TESTS
// when `type="command`, the implicit role is `menuitem`
{ code: '<menuitem type="command" aria-atomic />' },
{ code: '<menuitem type="command" aria-busy />' },
{ code: '<menuitem type="command" aria-controls />' },
{ code: '<menuitem type="command" aria-describedby />' },
{ code: '<menuitem type="command" aria-disabled />' },
{ code: '<menuitem type="command" aria-dropeffect />' },
{ code: '<menuitem type="command" aria-flowto />' },
{ code: '<menuitem type="command" aria-grabbed />' },
{ code: '<menuitem type="command" aria-haspopup />' },
{ code: '<menuitem type="command" aria-hidden />' },
{ code: '<menuitem type="command" aria-label />' },
{ code: '<menuitem type="command" aria-labelledby />' },
{ code: '<menuitem type="command" aria-live />' },
{ code: '<menuitem type="command" aria-owns />' },
{ code: '<menuitem type="command" aria-relevant />' },
// when `type="checkbox`, the implicit role is `menuitemcheckbox`
{ code: '<menuitem type="checkbox" aria-checked />' },
{ code: '<menuitem type="checkbox" aria-atomic />' },
{ code: '<menuitem type="checkbox" aria-busy />' },
{ code: '<menuitem type="checkbox" aria-controls />' },
{ code: '<menuitem type="checkbox" aria-describedby />' },
{ code: '<menuitem type="checkbox" aria-disabled />' },
{ code: '<menuitem type="checkbox" aria-dropeffect />' },
{ code: '<menuitem type="checkbox" aria-flowto />' },
{ code: '<menuitem type="checkbox" aria-grabbed />' },
{ code: '<menuitem type="checkbox" aria-haspopup />' },
{ code: '<menuitem type="checkbox" aria-hidden />' },
{ code: '<menuitem type="checkbox" aria-invalid />' },
{ code: '<menuitem type="checkbox" aria-label />' },
{ code: '<menuitem type="checkbox" aria-labelledby />' },
{ code: '<menuitem type="checkbox" aria-live />' },
{ code: '<menuitem type="checkbox" aria-owns />' },
{ code: '<menuitem type="checkbox" aria-relevant />' },
// when `type="radio`, the implicit role is `menuitemradio`
{ code: '<menuitem type="radio" aria-checked />' },
{ code: '<menuitem type="radio" aria-atomic />' },
{ code: '<menuitem type="radio" aria-busy />' },
{ code: '<menuitem type="radio" aria-controls />' },
{ code: '<menuitem type="radio" aria-describedby />' },
{ code: '<menuitem type="radio" aria-disabled />' },
{ code: '<menuitem type="radio" aria-dropeffect />' },
{ code: '<menuitem type="radio" aria-flowto />' },
{ code: '<menuitem type="radio" aria-grabbed />' },
{ code: '<menuitem type="radio" aria-haspopup />' },
{ code: '<menuitem type="radio" aria-hidden />' },
{ code: '<menuitem type="radio" aria-invalid />' },
{ code: '<menuitem type="radio" aria-label />' },
{ code: '<menuitem type="radio" aria-labelledby />' },
{ code: '<menuitem type="radio" aria-live />' },
{ code: '<menuitem type="radio" aria-owns />' },
{ code: '<menuitem type="radio" aria-relevant />' },
{ code: '<menuitem type="radio" aria-posinset />' },
{ code: '<menuitem type="radio" aria-setsize />' },
// these will have global
{ code: '<menuitem aria-checked />' },
{ code: '<menuitem type="foo" aria-checked />' },
// INPUT TESTS
// when `type="button"`, the implicit role is `button`
{ code: '<input type="button" aria-expanded />' },
{ code: '<input type="button" aria-pressed />' },
{ code: '<input type="button" aria-atomic />' },
{ code: '<input type="button" aria-busy />' },
{ code: '<input type="button" aria-controls />' },
{ code: '<input type="button" aria-describedby />' },
{ code: '<input type="button" aria-disabled />' },
{ code: '<input type="button" aria-dropeffect />' },
{ code: '<input type="button" aria-flowto />' },
{ code: '<input type="button" aria-grabbed />' },
{ code: '<input type="button" aria-haspopup />' },
{ code: '<input type="button" aria-hidden />' },
{ code: '<input type="button" aria-label />' },
{ code: '<input type="button" aria-labelledby />' },
{ code: '<input type="button" aria-live />' },
{ code: '<input type="button" aria-owns />' },
{ code: '<input type="button" aria-relevant />' },
// when `type="image"`, the implicit role is `button`
{ code: '<input type="image" aria-expanded />' },
{ code: '<input type="image" aria-pressed />' },
{ code: '<input type="image" aria-atomic />' },
{ code: '<input type="image" aria-busy />' },
{ code: '<input type="image" aria-controls />' },
{ code: '<input type="image" aria-describedby />' },
{ code: '<input type="image" aria-disabled />' },
{ code: '<input type="image" aria-dropeffect />' },
{ code: '<input type="image" aria-flowto />' },
{ code: '<input type="image" aria-grabbed />' },
{ code: '<input type="image" aria-haspopup />' },
{ code: '<input type="image" aria-hidden />' },
{ code: '<input type="image" aria-label />' },
{ code: '<input type="image" aria-labelledby />' },
{ code: '<input type="image" aria-live />' },
{ code: '<input type="image" aria-owns />' },
{ code: '<input type="image" aria-relevant />' },
// when `type="reset"`, the implicit role is `button`
{ code: '<input type="reset" aria-expanded />' },
{ code: '<input type="reset" aria-pressed />' },
{ code: '<input type="reset" aria-atomic />' },
{ code: '<input type="reset" aria-busy />' },
{ code: '<input type="reset" aria-controls />' },
{ code: '<input type="reset" aria-describedby />' },
{ code: '<input type="reset" aria-disabled />' },
{ code: '<input type="reset" aria-dropeffect />' },
{ code: '<input type="reset" aria-flowto />' },
{ code: '<input type="reset" aria-grabbed />' },
{ code: '<input type="reset" aria-haspopup />' },
{ code: '<input type="reset" aria-hidden />' },
{ code: '<input type="reset" aria-label />' },
{ code: '<input type="reset" aria-labelledby />' },
{ code: '<input type="reset" aria-live />' },
{ code: '<input type="reset" aria-owns />' },
{ code: '<input type="reset" aria-relevant />' },
// when `type="submit"`, the implicit role is `button`
{ code: '<input type="submit" aria-expanded />' },
{ code: '<input type="submit" aria-pressed />' },
{ code: '<input type="submit" aria-atomic />' },
{ code: '<input type="submit" aria-busy />' },
{ code: '<input type="submit" aria-controls />' },
{ code: '<input type="submit" aria-describedby />' },
{ code: '<input type="submit" aria-disabled />' },
{ code: '<input type="submit" aria-dropeffect />' },
{ code: '<input type="submit" aria-flowto />' },
{ code: '<input type="submit" aria-grabbed />' },
{ code: '<input type="submit" aria-haspopup />' },
{ code: '<input type="submit" aria-hidden />' },
{ code: '<input type="submit" aria-label />' },
{ code: '<input type="submit" aria-labelledby />' },
{ code: '<input type="submit" aria-live />' },
{ code: '<input type="submit" aria-owns />' },
{ code: '<input type="submit" aria-relevant />' },
// when `type="checkbox"`, the implicit role is `checkbox`
{ code: '<input type="checkbox" aria-atomic />' },
{ code: '<input type="checkbox" aria-busy />' },
{ code: '<input type="checkbox" aria-checked />' },
{ code: '<input type="checkbox" aria-controls />' },
{ code: '<input type="checkbox" aria-describedby />' },
{ code: '<input type="checkbox" aria-disabled />' },
{ code: '<input type="checkbox" aria-dropeffect />' },
{ code: '<input type="checkbox" aria-flowto />' },
{ code: '<input type="checkbox" aria-grabbed />' },
{ code: '<input type="checkbox" aria-hidden />' },
{ code: '<input type="checkbox" aria-invalid />' },
{ code: '<input type="checkbox" aria-label />' },
{ code: '<input type="checkbox" aria-labelledby />' },
{ code: '<input type="checkbox" aria-live />' },
{ code: '<input type="checkbox" aria-owns />' },
{ code: '<input type="checkbox" aria-relevant />' },
// when `type="radio"`, the implicit role is `radio`
{ code: '<input type="radio" aria-atomic />' },
{ code: '<input type="radio" aria-busy />' },
{ code: '<input type="radio" aria-checked />' },
{ code: '<input type="radio" aria-controls />' },
{ code: '<input type="radio" aria-describedby />' },
{ code: '<input type="radio" aria-disabled />' },
{ code: '<input type="radio" aria-dropeffect />' },
{ code: '<input type="radio" aria-flowto />' },
{ code: '<input type="radio" aria-grabbed />' },
{ code: '<input type="radio" aria-hidden />' },
{ code: '<input type="radio" aria-label />' },
{ code: '<input type="radio" aria-labelledby />' },
{ code: '<input type="radio" aria-live />' },
{ code: '<input type="radio" aria-owns />' },
{ code: '<input type="radio" aria-relevant />' },
{ code: '<input type="radio" aria-posinset />' },
{ code: '<input type="radio" aria-setsize />' },
// when `type="range"`, the implicit role is `slider`
{ code: '<input type="range" aria-valuemax />' },
{ code: '<input type="range" aria-valuemin />' },
{ code: '<input type="range" aria-valuenow />' },
{ code: '<input type="range" aria-orientation />' },
{ code: '<input type="range" aria-atomic />' },
{ code: '<input type="range" aria-busy />' },
{ code: '<input type="range" aria-controls />' },
{ code: '<input type="range" aria-describedby />' },
{ code: '<input type="range" aria-disabled />' },
{ code: '<input type="range" aria-dropeffect />' },
{ code: '<input type="range" aria-flowto />' },
{ code: '<input type="range" aria-grabbed />' },
{ code: '<input type="range" aria-haspopup />' },
{ code: '<input type="range" aria-hidden />' },
{ code: '<input type="range" aria-invalid />' },
{ code: '<input type="range" aria-label />' },
{ code: '<input type="range" aria-labelledby />' },
{ code: '<input type="range" aria-live />' },
{ code: '<input type="range" aria-owns />' },
{ code: '<input type="range" aria-relevant />' },
{ code: '<input type="range" aria-valuetext />' },
// these will have role of `textbox`,
{ code: '<input type="email" aria-disabled />' },
{ code: '<input type="password" aria-disabled />' },
{ code: '<input type="search" aria-disabled />' },
{ code: '<input type="tel" aria-disabled />' },
{ code: '<input type="url" aria-disabled />' },
{ code: '<input aria-disabled />' },
// Allow null/undefined values regardless of role
{ code: '<h2 role="presentation" aria-level={null} />' },
{ code: '<h2 role="presentation" aria-level={undefined} />' },
// OTHER TESTS
{ code: '<button aria-pressed />' },
{ code: '<form aria-hidden />' },
{ code: '<h1 aria-hidden />' },
{ code: '<h2 aria-hidden />' },
{ code: '<h3 aria-hidden />' },
{ code: '<h4 aria-hidden />' },
{ code: '<h5 aria-hidden />' },
{ code: '<h6 aria-hidden />' },
{ code: '<hr aria-hidden />' },
{ code: '<li aria-current />' },
{ code: '<meter aria-atomic />' },
{ code: '<option aria-atomic />' },
{ code: '<progress aria-atomic />' },
{ code: '<textarea aria-hidden />' },
{ code: '<select aria-expanded />' },
{ code: '<datalist aria-expanded />' },
{ code: '<div role="heading" aria-level />' },
{ code: '<div role="heading" aria-level="1" />' },
semver.satisfies(eslintVersion, '>= 6') ? {
code: `
const HelloThere = () => (
<Hello
role="searchbox"
frag={
<>
<div>Hello</div>
<div>There</div>
</>
}
/>
);
const Hello = (props) => <div>{props.frag}</div>;
`,
} : [],
validTests,
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// implicit basic checks
{
code: '<a href="#" aria-checked />',
errors: [errorMessage('aria-checked', 'link', 'a', true)],
},
{
code: '<area href="#" aria-checked />',
errors: [errorMessage('aria-checked', 'link', 'area', true)],
},
{
code: '<link href="#" aria-checked />',
errors: [errorMessage('aria-checked', 'link', 'link', true)],
},
{
code: '<img alt="foobar" aria-checked />',
errors: [errorMessage('aria-checked', 'img', 'img', true)],
},
{
code: '<menu type="toolbar" aria-checked />',
errors: [errorMessage('aria-checked', 'toolbar', 'menu', true)],
},
{
code: '<aside aria-checked />',
errors: [errorMessage('aria-checked', 'complementary', 'aside', true)],
},
{
code: '<ul aria-expanded />',
errors: [errorMessage('aria-expanded', 'list', 'ul', true)],
},
{
code: '<details aria-expanded />',
errors: [errorMessage('aria-expanded', 'group', 'details', true)],
},
{
code: '<dialog aria-expanded />',
errors: [errorMessage('aria-expanded', 'dialog', 'dialog', true)],
},
{
code: '<aside aria-expanded />',
errors: [errorMessage('aria-expanded', 'complementary', 'aside', true)],
},
{
code: '<article aria-expanded />',
errors: [errorMessage('aria-expanded', 'article', 'article', true)],
},
{
code: '<body aria-expanded />',
errors: [errorMessage('aria-expanded', 'document', 'body', true)],
},
{
code: '<li aria-expanded />',
errors: [errorMessage('aria-expanded', 'listitem', 'li', true)],
},
{
code: '<nav aria-expanded />',
errors: [errorMessage('aria-expanded', 'navigation', 'nav', true)],
},
{
code: '<ol aria-expanded />',
errors: [errorMessage('aria-expanded', 'list', 'ol', true)],
},
{
code: '<output aria-expanded />',
errors: [errorMessage('aria-expanded', 'status', 'output', true)],
},
{
code: '<section aria-expanded />',
errors: [errorMessage('aria-expanded', 'region', 'section', true)],
},
{
code: '<tbody aria-expanded />',
errors: [errorMessage('aria-expanded', 'rowgroup', 'tbody', true)],
},
{
code: '<tfoot aria-expanded />',
errors: [errorMessage('aria-expanded', 'rowgroup', 'tfoot', true)],
},
{
code: '<thead aria-expanded />',
errors: [errorMessage('aria-expanded', 'rowgroup', 'thead', true)],
},
{
code: '<input type="radio" aria-invalid />',
errors: [errorMessage('aria-invalid', 'radio', 'input', true)],
},
{
code: '<input type="radio" aria-selected />',
errors: [errorMessage('aria-selected', 'radio', 'input', true)],
},
{
code: '<input type="radio" aria-haspopup />',
errors: [errorMessage('aria-haspopup', 'radio', 'input', true)],
},
{
code: '<input type="checkbox" aria-haspopup />',
errors: [errorMessage('aria-haspopup', 'checkbox', 'input', true)],
},
{
code: '<input type="reset" aria-invalid />',
errors: [errorMessage('aria-invalid', 'button', 'input', true)],
},
{
code: '<input type="submit" aria-invalid />',
errors: [errorMessage('aria-invalid', 'button', 'input', true)],
},
{
code: '<input type="image" aria-invalid />',
errors: [errorMessage('aria-invalid', 'button', 'input', true)],
},
{
code: '<input type="button" aria-invalid />',
errors: [errorMessage('aria-invalid', 'button', 'input', true)],
},
{
code: '<menuitem type="command" aria-invalid />',
errors: [errorMessage('aria-invalid', 'menuitem', 'menuitem', true)],
},
{
code: '<menuitem type="radio" aria-selected />',
errors: [errorMessage('aria-selected', 'menuitemradio', 'menuitem', true)],
},
{
code: '<menu type="toolbar" aria-haspopup />',
errors: [errorMessage('aria-haspopup', 'toolbar', 'menu', true)],
},
{
code: '<menu type="toolbar" aria-invalid />',
errors: [errorMessage('aria-invalid', 'toolbar', 'menu', true)],
},
{
code: '<menu type="toolbar" aria-expanded />',
errors: [errorMessage('aria-expanded', 'toolbar', 'menu', true)],
},
{
code: '<link href="#" aria-invalid />',
errors: [errorMessage('aria-invalid', 'link', 'link', true)],
},
{
code: '<area href="#" aria-invalid />',
errors: [errorMessage('aria-invalid', 'link', 'area', true)],
},
{
code: '<a href="#" aria-invalid />',
errors: [errorMessage('aria-invalid', 'link', 'a', true)],
},
{
code: '<Link href="#" aria-checked />',
errors: [errorMessage('aria-checked', 'link', 'a', true)],
settings: componentsSettings,
},
)).concat(invalidTests).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,50 @@
/**
* @fileoverview Enforce scope prop is only used on <th> elements.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/scope';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'The scope prop can only be used on <th> elements.',
type: 'JSXAttribute',
};
const componentsSettings = {
'jsx-a11y': {
components: {
Foo: 'div',
TableHeader: 'th',
},
},
};
ruleTester.run('scope', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<div foo />;' },
{ code: '<th scope />' },
{ code: '<th scope="row" />' },
{ code: '<th scope={foo} />' },
{ code: '<th scope={"col"} {...props} />' },
{ code: '<Foo scope="bar" {...props} />' },
{ code: '<TableHeader scope="row" />', settings: componentsSettings },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div scope />', errors: [expectedError] },
{ code: '<Foo scope="bar" />', settings: componentsSettings, errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,55 @@
/**
* @fileoverview Enforce tabIndex value is not greater than zero.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/tabindex-no-positive';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Avoid positive integer values for tabIndex.',
type: 'JSXAttribute',
};
ruleTester.run('tabindex-no-positive', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<div {...props} />' },
{ code: '<div id="main" />' },
{ code: '<div tabIndex={undefined} />' },
{ code: '<div tabIndex={`${undefined}`} />' },
{ code: '<div tabIndex={`${undefined}${undefined}`} />' },
{ code: '<div tabIndex={0} />' },
{ code: '<div tabIndex={-1} />' },
{ code: '<div tabIndex={null} />' },
{ code: '<div tabIndex={bar()} />' },
{ code: '<div tabIndex={bar} />' },
{ code: '<div tabIndex={"foobar"} />' },
{ code: '<div tabIndex="0" />' },
{ code: '<div tabIndex="-1" />' },
{ code: '<div tabIndex="-5" />' },
{ code: '<div tabIndex="-5.5" />' },
{ code: '<div tabIndex={-5.5} />' },
{ code: '<div tabIndex={-5} />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div tabIndex="1" />', errors: [expectedError] },
{ code: '<div tabIndex={1} />', errors: [expectedError] },
{ code: '<div tabIndex={"1"} />', errors: [expectedError] },
{ code: '<div tabIndex={`1`} />', errors: [expectedError] },
{ code: '<div tabIndex={1.589} />', errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,91 @@
import test from 'tape';
import attributesComparator from '../../../src/util/attributesComparator';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
test('attributesComparator', (t) => {
t.equal(
attributesComparator(),
true,
'baseAttributes are undefined and attributes are undefined -> true',
);
t.equal(
attributesComparator([], []),
true,
'baseAttributes are empty and attributes are empty -> true',
);
t.equal(
attributesComparator([], [
JSXAttributeMock('foo', 0),
JSXAttributeMock('bar', 'baz'),
]),
true,
'baseAttributes are empty and attributes have values -> true',
);
const baseAttributes = [
{
name: 'biz',
value: 1,
}, {
name: 'fizz',
value: 'pop',
}, {
name: 'fuzz',
value: 'lolz',
},
];
t.equal(
attributesComparator(baseAttributes, []),
false,
'baseAttributes have values and attributes are empty -> false',
);
t.equal(
attributesComparator(baseAttributes, [
JSXElementMock(),
JSXAttributeMock('biz', 2),
JSXAttributeMock('ziff', 'opo'),
JSXAttributeMock('far', 'lolz'),
]),
false,
'baseAttributes have values and attributes have values, and the values are different -> false',
);
t.equal(
attributesComparator(baseAttributes, [
JSXAttributeMock('biz', 1),
JSXAttributeMock('fizz', 'pop'),
JSXAttributeMock('goo', 'gazz'),
]),
false,
'baseAttributes have values and attributes have values, and the values are a subset -> false',
);
t.equal(
attributesComparator(baseAttributes, [
JSXAttributeMock('biz', 1),
JSXAttributeMock('fizz', 'pop'),
JSXAttributeMock('fuzz', 'lolz'),
]),
true,
'baseAttributes have values and attributes have values, and the values are the same -> true',
);
t.equal(
attributesComparator(baseAttributes, [
JSXAttributeMock('biz', 1),
JSXAttributeMock('fizz', 'pop'),
JSXAttributeMock('fuzz', 'lolz'),
JSXAttributeMock('dar', 'tee'),
]),
true,
'baseAttributes have values and attributes have values, and the values are a superset -> true',
);
t.end();
});

View File

@@ -0,0 +1,174 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import getAccessibleChildText from '../../../src/util/getAccessibleChildText';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
test('getAccessibleChildText', (t) => {
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[JSXAttributeMock('aria-label', 'foo')],
), elementType),
'foo',
'returns the aria-label when present',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[JSXAttributeMock('aria-label', 'foo')],
[{ type: 'JSXText', value: 'bar' }],
), elementType),
'foo',
'returns the aria-label instead of children',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[JSXAttributeMock('aria-hidden', 'true')],
), elementType),
'',
'skips elements with aria-hidden=true',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'JSXText', value: 'bar' }],
), elementType),
'bar',
'returns literal value for JSXText child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
JSXAttributeMock('alt', 'a sensible label'),
])],
), elementType),
'a sensible label',
'returns alt text for img child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[JSXElementMock('span', [
JSXAttributeMock('alt', 'a sensible label'),
])],
), elementType),
'',
'returns blank when alt tag is used on arbitrary element',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'bar' }],
), elementType),
'bar',
'returns literal value for JSXText child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: ' bar ' }],
), elementType),
'bar',
'returns trimmed literal value for JSXText child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'foo bar' }],
), elementType),
'foo bar',
'returns space-collapsed literal value for JSXText child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'foo, bar. baz? foo; bar:' }],
), elementType),
'foo bar baz foo bar',
'returns punctuation-stripped literal value for JSXText child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[JSXElementMock(
'span',
[],
[{ type: 'Literal', value: 'bar' }],
)],
), elementType),
'bar',
'returns recursive value for JSXElement child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[JSXElementMock(
'span',
[],
[JSXElementMock(
'span',
[JSXAttributeMock('aria-hidden', 'true')],
)],
)],
), elementType),
'',
'skips children with aria-hidden-true',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'foo' }, { type: 'Literal', value: 'bar' }],
), elementType),
'foo bar',
'joins multiple children properly - no spacing',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: ' foo ' }, { type: 'Literal', value: ' bar ' }],
), elementType),
'foo bar',
'joins multiple children properly - with spacing',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'foo' }, { type: 'Unknown' }, { type: 'Literal', value: 'bar' }],
), elementType),
'foo bar',
'skips unknown elements',
);
t.end();
});

View File

@@ -0,0 +1,71 @@
import test from 'tape';
import getComputedRole from '../../../src/util/getComputedRole';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('getComputedRole', (t) => {
t.equal(
getComputedRole(
'div',
[JSXAttributeMock('role', 'button')],
),
'button',
'explicit role + valid role -> returns the role',
);
t.equal(
getComputedRole(
'li',
[JSXAttributeMock('role', 'beeswax')],
),
'listitem',
'explicit role + invalid role + has implicit -> returns the implicit role',
);
t.equal(
getComputedRole(
'div',
[JSXAttributeMock('role', 'beeswax')],
),
null,
'explicit role + invalid role + lacks implicit -> returns null',
);
t.equal(
getComputedRole(
'li',
[],
),
'listitem',
'explicit role + no role + has implicit -> returns the implicit role',
);
t.equal(
getComputedRole(
'div',
[],
),
null,
'explicit role + no role + lacks implicit -> returns null',
);
t.equal(
getComputedRole(
'li',
[JSXAttributeMock('role', 'beeswax')],
),
'listitem',
'implicit role + has implicit -> returns the implicit role',
);
t.equal(
getComputedRole(
'div',
[],
),
null,
'implicit role + lacks implicit -> returns null',
);
t.end();
});

View File

@@ -0,0 +1,154 @@
import test from 'tape';
import getElementType from '../../../src/util/getElementType';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('getElementType', (t) => {
t.test('no settings in context', (st) => {
const elementType = getElementType({ settings: {} });
st.equal(
elementType(JSXElementMock('input').openingElement),
'input',
'returns the exact tag name for a DOM element',
);
st.equal(
elementType(JSXElementMock('CustomInput').openingElement),
'CustomInput',
'returns the exact tag name for a custom element',
);
st.equal(
elementType(JSXElementMock('toString').openingElement),
'toString',
'returns the exact tag name for names that are in Object.prototype',
);
st.equal(
elementType(JSXElementMock('span', [JSXAttributeMock('as', 'h1')]).openingElement),
'span',
'returns the default tag name provided',
);
st.end();
});
t.test('components settings in context', (st) => {
const elementType = getElementType({
settings: {
'jsx-a11y': {
components: {
CustomInput: 'input',
},
},
},
});
st.equal(
elementType(JSXElementMock('input').openingElement),
'input',
'returns the exact tag name for a DOM element',
);
st.equal(
elementType(JSXElementMock('CustomInput').openingElement),
'input',
'returns the mapped tag name for a custom element',
);
st.equal(
elementType(JSXElementMock('CityInput').openingElement),
'CityInput',
'returns the exact tag name for a custom element not in the components map',
);
st.equal(
elementType(JSXElementMock('span', [JSXAttributeMock('as', 'h1')]).openingElement),
'span',
'return the default tag name since not polymorphicPropName was provided',
);
st.end();
});
t.test('polymorphicPropName settings in context', (st) => {
const elementType = getElementType({
settings: {
'jsx-a11y': {
polymorphicPropName: 'asChild',
components: {
CustomButton: 'button',
},
},
},
});
st.equal(
elementType(JSXElementMock('span', [JSXAttributeMock('asChild', 'h1')]).openingElement),
'h1',
'returns the tag name provided by the polymorphic prop, "asChild", defined in the settings',
);
st.equal(
elementType(JSXElementMock('CustomButton', [JSXAttributeMock('asChild', 'a')]).openingElement),
'a',
'returns the tag name provided by the polymorphic prop, "asChild", defined in the settings instead of the component mapping tag',
);
st.equal(
elementType(JSXElementMock('CustomButton', [JSXAttributeMock('as', 'a')]).openingElement),
'button',
'returns the tag name provided by the componnet mapping if the polymorphic prop, "asChild", defined in the settings is not set',
);
st.end();
});
t.test('polymorphicPropName settings and explicitly defined polymorphicAllowList in context', (st) => {
const elementType = getElementType({
settings: {
'jsx-a11y': {
polymorphicPropName: 'asChild',
polymorphicAllowList: [
'Box',
'Icon',
],
components: {
Box: 'div',
Icon: 'svg',
},
},
},
});
st.equal(
elementType(JSXElementMock('Spinner', [JSXAttributeMock('asChild', 'img')]).openingElement),
'Spinner',
'does not use the polymorphic prop if polymorphicAllowList is defined, but element is not part of polymorphicAllowList',
);
st.equal(
elementType(JSXElementMock('Icon', [JSXAttributeMock('asChild', 'img')]).openingElement),
'img',
'uses the polymorphic prop if it is in explicitly defined polymorphicAllowList',
);
st.equal(
elementType(JSXElementMock('Box', [JSXAttributeMock('asChild', 'span')]).openingElement),
'span',
'returns the tag name provided by the polymorphic prop, "asChild", defined in the settings instead of the component mapping tag',
);
st.equal(
elementType(JSXElementMock('Box', [JSXAttributeMock('as', 'a')]).openingElement),
'div',
'returns the tag name provided by the component mapping if the polymorphic prop, "asChild", defined in the settings is not set',
);
st.end();
});
t.end();
});

View File

@@ -0,0 +1,35 @@
import test from 'tape';
import getExplicitRole from '../../../src/util/getExplicitRole';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('getExplicitRole', (t) => {
t.equal(
getExplicitRole(
'div',
[JSXAttributeMock('role', 'button')],
),
'button',
'valid role returns the role',
);
t.equal(
getExplicitRole(
'div',
[JSXAttributeMock('role', 'beeswax')],
),
null,
'invalid role returns null',
);
t.equal(
getExplicitRole(
'div',
[],
),
null,
'no role returns null',
);
t.end();
});

View File

@@ -0,0 +1,25 @@
import test from 'tape';
import getImplicitRole from '../../../src/util/getImplicitRole';
test('getImplicitRole', (t) => {
t.equal(
getImplicitRole(
'li',
[],
),
'listitem',
'has implicit, returns implicit role',
);
t.equal(
getImplicitRole(
'div',
[],
),
null,
'lacks implicit, returns null',
);
t.end();
});

View File

@@ -0,0 +1,33 @@
import test from 'tape';
import getSuggestion from '../../../src/util/getSuggestion';
test('spell check suggestion API', (t) => {
t.deepEqual([], getSuggestion('foo'), 'returns no suggestions given empty word and no dictionary');
t.deepEqual(
getSuggestion('foo'),
[],
'returns no suggestions given real word and no dictionary',
);
t.deepEqual(
getSuggestion('fo', ['foo', 'bar', 'baz']),
['foo'],
'returns correct suggestion given real word and a dictionary',
);
t.deepEqual(
getSuggestion('theer', ['there', 'their', 'foo', 'bar']),
['there', 'their'],
'returns multiple correct suggestions given real word and a dictionary',
);
t.deepEqual(
getSuggestion('theer', ['there', 'their', 'foo', 'bar'], 1),
['there'],
'returns correct # of suggestions given the limit argument',
);
t.end();
});

View File

@@ -0,0 +1,85 @@
import test from 'tape';
import getTabIndex from '../../../src/util/getTabIndex';
import IdentifierMock from '../../../__mocks__/IdentifierMock';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('getTabIndex', (t) => {
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', 0)),
0,
'tabIndex is defined as zero -> zero',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', 1)),
1,
'tabIndex is defined as a positive integer -> returns it',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', -1)),
-1,
'tabIndex is defined as a negative integer -> returns it',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', '')),
undefined,
'tabIndex is defined as an empty string -> undefined',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', 9.1)),
undefined,
'tabIndex is defined as a float -> undefined',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', '0')),
0,
'tabIndex is defined as a string which converts to a number -> returns the integer',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', '0a')),
undefined,
'tabIndex is defined as a string which is NaN -> returns undefined',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', true)),
undefined,
'tabIndex is defined as true -> returns undefined',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', false)),
undefined,
'tabIndex is defined as false -> returns undefined',
);
t.equal(
typeof getTabIndex(JSXAttributeMock('tabIndex', () => 0)),
'function',
'tabIndex is defined as a function expression -> returns the correct type',
);
const name = 'identName';
t.equal(
getTabIndex(JSXAttributeMock(
'tabIndex',
IdentifierMock(name),
true,
)),
name,
'tabIndex is defined as a variable expression -> returns the Identifier name',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', undefined)),
undefined,
'tabIndex is not defined -> returns undefined',
);
t.end();
});

View File

@@ -0,0 +1,157 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import hasAccessibleChild from '../../../src/util/hasAccessibleChild';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXExpressionContainerMock from '../../../__mocks__/JSXExpressionContainerMock';
test('hasAccessibleChild', (t) => {
t.equal(
hasAccessibleChild(JSXElementMock('div', []), elementType),
false,
'has no children and does not set dangerouslySetInnerHTML -> false',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [JSXAttributeMock('dangerouslySetInnerHTML', true)], []),
elementType,
),
true,
'has no children and sets dangerouslySetInnerHTML -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock(
'div',
[],
[{
type: 'Literal',
value: 'foo',
}],
),
elementType,
),
true,
'has children + Literal child -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [JSXElementMock('div', [])]),
elementType,
),
true,
'has children + visible JSXElement child -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [{
type: 'JSXText',
value: 'foo',
}]),
elementType,
),
true,
'has children + JSText element -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXElementMock('div', [
JSXAttributeMock('aria-hidden', true),
]),
]),
elementType,
),
false,
'has children + hidden child JSXElement -> false',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXExpressionContainerMock({
type: 'Identifier',
name: 'foo',
}),
]),
elementType,
),
true,
'defined JSXExpressionContainer -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXExpressionContainerMock({
type: 'Identifier',
name: 'undefined',
}),
]),
elementType,
),
false,
'has children + undefined JSXExpressionContainer -> false',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [{
type: 'Unknown',
}]),
elementType,
),
false,
'unknown child type -> false',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [JSXAttributeMock('children', true)], []),
elementType,
),
true,
'children passed as a prop -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXElementMock('input', [JSXAttributeMock('type', 'hidden')]),
]),
elementType,
),
false,
'has chidren -> hidden child input JSXElement -> false',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXElementMock('CustomInput', [JSXAttributeMock('type', 'hidden')]),
]),
elementType,
),
true,
'has children + custom JSXElement of type hidden -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXElementMock('CustomInput', [JSXAttributeMock('type', 'hidden')]),
]),
() => 'input',
),
false,
'custom JSXElement mapped to input if type is hidden -> false',
);
t.end();
});

View File

@@ -0,0 +1,87 @@
import test from 'tape';
import JSXAttributeMock from '../../../../__mocks__/JSXAttributeMock';
import getImplicitRoleForInput from '../../../../src/util/implicitRoles/input';
test('isAbstractRole', (t) => {
t.test('works for buttons', (st) => {
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'button')]),
'button',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'image')]),
'button',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'reset')]),
'button',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'submit')]),
'button',
);
st.end();
});
t.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'checkbox')]),
'checkbox',
'works for checkboxes',
);
t.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'radio')]),
'radio',
'works for radios',
);
t.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'range')]),
'slider',
'works for ranges',
);
t.test('works for textboxes', (st) => {
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'email')]),
'textbox',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'password')]),
'textbox',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'search')]),
'textbox',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'tel')]),
'textbox',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'url')]),
'textbox',
);
st.end();
});
t.equal(
getImplicitRoleForInput([JSXAttributeMock('type', '')]),
'textbox',
'works for the default case',
);
t.equal(
getImplicitRoleForInput([JSXAttributeMock('type', true)]),
'textbox',
'works for the true case',
);
t.end();
});

View File

@@ -0,0 +1,20 @@
import test from 'tape';
import JSXAttributeMock from '../../../../__mocks__/JSXAttributeMock';
import getImplicitRoleForMenu from '../../../../src/util/implicitRoles/menu';
test('isAbstractRole', (t) => {
t.equal(
getImplicitRoleForMenu([JSXAttributeMock('type', 'toolbar')]),
'toolbar',
'works for toolbars',
);
t.equal(
getImplicitRoleForMenu([JSXAttributeMock('type', '')]),
'',
'works for non-toolbars',
);
t.end();
});

View File

@@ -0,0 +1,38 @@
import test from 'tape';
import JSXAttributeMock from '../../../../__mocks__/JSXAttributeMock';
import getImplicitRoleForMenuitem from '../../../../src/util/implicitRoles/menuitem';
test('isAbstractRole', (t) => {
t.equal(
getImplicitRoleForMenuitem([JSXAttributeMock('type', 'command')]),
'menuitem',
'works for menu items',
);
t.equal(
getImplicitRoleForMenuitem([JSXAttributeMock('type', 'checkbox')]),
'menuitemcheckbox',
'works for menu item checkboxes',
);
t.equal(
getImplicitRoleForMenuitem([JSXAttributeMock('type', 'radio')]),
'menuitemradio',
'works for menu item radios',
);
t.equal(
getImplicitRoleForMenuitem([JSXAttributeMock('type', '')]),
'',
'works for non-toolbars',
);
t.equal(
getImplicitRoleForMenuitem([JSXAttributeMock('type', true)]),
'',
'works for the true case',
);
t.end();
});

View File

@@ -0,0 +1,51 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isAbstractRole from '../../../src/util/isAbstractRole';
import {
genElementSymbol,
genAbstractRoleElements,
genNonAbstractRoleElements,
} from '../../../__mocks__/genInteractives';
test('isAbstractRole', (t) => {
t.equal(
isAbstractRole(undefined, []),
false,
'does NOT identify JSX Components (no tagName) as abstract role elements',
);
t.test('elements with an abstract role', (st) => {
genAbstractRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isAbstractRole(
elementType(openingElement),
attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as an abstract role element`,
);
});
st.end();
});
t.test('elements with a non-abstract role', (st) => {
genNonAbstractRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isAbstractRole(
elementType(openingElement),
attributes,
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` as an abstract role element`,
);
});
st.end();
});
t.end();
});

View File

@@ -0,0 +1,52 @@
import test from 'tape';
import isContentEditable from '../../../src/util/isContentEditable';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('isContentEditable - HTML5', (t) => {
t.equal(
isContentEditable('some tag', [
JSXAttributeMock('contentEditable', 'true'),
]),
true,
'identifies HTML5 contentEditable elements',
);
t.test('not content editable', (st) => {
st.equal(
isContentEditable('some tag', [
JSXAttributeMock('contentEditable', null),
]),
false,
'does not identify HTML5 content editable elements with null as the value',
);
st.equal(
isContentEditable('some tag', [
JSXAttributeMock('contentEditable', undefined),
]),
false,
'does not identify HTML5 content editable elements with undefined as the value',
);
st.equal(
isContentEditable('some tag', [
JSXAttributeMock('contentEditable', true),
]),
false,
'does not identify HTML5 content editable elements with true as the value',
);
st.equal(
isContentEditable('some tag', [
JSXAttributeMock('contentEditable', 'false'),
]),
false,
'does not identify HTML5 content editable elements with "false" as the value',
);
st.end();
});
t.end();
});

View File

@@ -0,0 +1,30 @@
import test from 'tape';
import { dom } from 'aria-query';
import { elementType } from 'jsx-ast-utils';
import isDOMElement from '../../../src/util/isDOMElement';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
test('isDOMElement', (t) => {
t.test('DOM elements', (st) => {
dom.forEach((_, el) => {
const element = JSXElementMock(el);
st.equal(
isDOMElement(elementType(element.openingElement)),
true,
`identifies ${el} as a DOM element`,
);
});
st.end();
});
t.equal(
isDOMElement(JSXElementMock('CustomElement')),
false,
'does not identify a custom element',
);
t.end();
});

View File

@@ -0,0 +1,88 @@
import test from 'tape';
import isDisabledElement from '../../../src/util/isDisabledElement';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('isDisabledElement', (t) => {
t.test('HTML5', (st) => {
st.equal(
isDisabledElement([
JSXAttributeMock('disabled', 'disabled'),
]),
true,
'identifies HTML5 disabled elements',
);
st.equal(
isDisabledElement([
JSXAttributeMock('disabled', null),
]),
true,
'identifies HTML5 disabled elements with null as the value',
);
st.equal(
isDisabledElement([
JSXAttributeMock('disabled', undefined),
]),
false,
'does not identify HTML5 disabled elements with undefined as the value',
);
st.end();
});
t.test('ARIA', (st) => {
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', 'true'),
]),
true,
'does not identify ARIA disabled elements',
);
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', true),
]),
true,
'does not identify ARIA disabled elements',
);
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', 'false'),
]),
false,
'does not identify ARIA disabled elements',
);
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', false),
]),
false,
'does not identify ARIA disabled elements',
);
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', null),
]),
false,
'does not identify ARIA disabled elements with null as the value',
);
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', undefined),
]),
false,
'does not identify ARIA disabled elements with undefined as the value',
);
st.end();
});
t.end();
});

View File

@@ -0,0 +1,111 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isFocusable from '../../../src/util/isFocusable';
import {
genElementSymbol,
genInteractiveElements,
genNonInteractiveElements,
} from '../../../__mocks__/genInteractives';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
function mergeTabIndex(index, attributes) {
return [].concat(attributes, JSXAttributeMock('tabIndex', index));
}
test('isFocusable', (t) => {
t.test('interactive elements', (st) => {
genInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isFocusable(
elementType(openingElement),
openingElement.attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(-1, openingElement.attributes),
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` with tabIndex of -1 as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(0, openingElement.attributes),
),
true,
`identifies \`${genElementSymbol(openingElement)}\` with tabIndex of 0 as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(1, openingElement.attributes),
),
true,
`identifies \`${genElementSymbol(openingElement)}\` with tabIndex of 1 as a focusable element`,
);
});
st.end();
});
t.test('non-interactive elements', (st) => {
genNonInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isFocusable(
elementType(openingElement),
openingElement.attributes,
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(-1, openingElement.attributes),
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` with tabIndex of -1 as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(0, openingElement.attributes),
),
true,
`identifies \`${genElementSymbol(openingElement)}\` with tabIndex of 0 as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(1, openingElement.attributes),
),
true,
`identifies \`${genElementSymbol(openingElement)}\` with tabIndex of 1 as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex('bogus', openingElement.attributes),
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` with tabIndex of 'bogus' as a focusable element`,
);
});
st.end();
});
t.end();
});

View File

@@ -0,0 +1,104 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isInteractiveElement from '../../../src/util/isInteractiveElement';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import {
genElementSymbol,
genIndeterminantInteractiveElements,
genInteractiveElements,
genInteractiveRoleElements,
genNonInteractiveElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
test('isInteractiveElement', (t) => {
t.equal(
isInteractiveElement(undefined, []),
false,
'identifies them as interactive elements',
);
t.test('interactive elements', (st) => {
genInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive element`,
);
});
st.end();
});
t.test('interactive role elements', (st) => {
genInteractiveRoleElements().forEach(({ openingElement }) => {
st.equal(
isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive element`,
);
});
st.end();
});
t.test('non-interactive elements', (st) => {
genNonInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive element`,
);
});
st.end();
});
t.test('non-interactive role elements', (st) => {
genNonInteractiveRoleElements().forEach(({ openingElement }) => {
st.equal(
isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive element`,
);
});
st.end();
});
t.test('indeterminate elements', (st) => {
genIndeterminantInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive element`,
);
});
st.end();
});
t.equal(
isInteractiveElement('CustomComponent', JSXElementMock()),
false,
'JSX elements are not interactive',
);
t.end();
});

View File

@@ -0,0 +1,59 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isInteractiveRole from '../../../src/util/isInteractiveRole';
import {
genElementSymbol,
genInteractiveRoleElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
test('isInteractiveRole', (t) => {
t.equal(
isInteractiveRole(undefined, []),
false,
'identifies JSX Components (no tagName) as interactive role elements',
);
t.test('elements with a non-interactive role', (st) => {
genNonInteractiveRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isInteractiveRole(
elementType(openingElement),
attributes,
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` as an interactive role element`,
);
});
st.end();
});
t.equal(
isInteractiveRole('div', []),
false,
'does NOT identify elements without a role as interactive role elements',
);
t.test('elements with an interactive role', (st) => {
genInteractiveRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isInteractiveRole(
elementType(openingElement),
attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive role element`,
);
});
st.end();
});
t.end();
});

View File

@@ -0,0 +1,97 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isNonInteractiveElement from '../../../src/util/isNonInteractiveElement';
import {
genElementSymbol,
genIndeterminantInteractiveElements,
genInteractiveElements,
genInteractiveRoleElements,
genNonInteractiveElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
test('isNonInteractiveElement', (t) => {
t.equal(
isNonInteractiveElement(undefined, []),
false,
'identifies JSX Components (no tagName) as non-interactive elements',
);
t.test('non-interactive elements', (st) => {
genNonInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive element`,
);
});
st.end();
});
t.test('non-interactive role elements', (st) => {
genNonInteractiveRoleElements().forEach(({ openingElement }) => {
st.equal(
isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive element`,
);
});
st.end();
});
t.test('interactive elements', (st) => {
genInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive element`,
);
});
st.end();
});
t.test('interactive role elements', (st) => {
genInteractiveRoleElements().forEach(({ openingElement }) => {
st.equal(
isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive element`,
);
});
st.end();
});
t.test('indeterminate elements', (st) => {
genIndeterminantInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive element`,
);
});
st.end();
});
t.end();
});

View File

@@ -0,0 +1,59 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isNonInteractiveRole from '../../../src/util/isNonInteractiveRole';
import {
genElementSymbol,
genInteractiveRoleElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
test('isNonInteractiveRole', (t) => {
t.equal(
isNonInteractiveRole(undefined, []),
false,
'identifies JSX Components (no tagName) as non-interactive elements',
);
t.test('elements with a non-interactive role', (st) => {
genNonInteractiveRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isNonInteractiveRole(
elementType(openingElement),
attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive role element`,
);
});
st.end();
});
t.equal(
isNonInteractiveRole('div', []),
false,
'does NOT identify elements without a role as non-interactive role elements',
);
t.test('elements with an interactive role', (st) => {
genInteractiveRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isNonInteractiveRole(
elementType(openingElement),
attributes,
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` as a non-interactive role element`,
);
});
st.end();
});
t.end();
});

View File

@@ -0,0 +1,52 @@
import test from 'tape';
import isNonLiteralProperty from '../../../src/util/isNonLiteralProperty';
import IdentifierMock from '../../../__mocks__/IdentifierMock';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXSpreadAttributeMock from '../../../__mocks__/JSXSpreadAttributeMock';
import JSXTextMock from '../../../__mocks__/JSXTextMock';
import LiteralMock from '../../../__mocks__/LiteralMock';
const theProp = 'theProp';
const spread = JSXSpreadAttributeMock('theSpread');
test('isNonLiteralProperty', (t) => {
t.equal(
isNonLiteralProperty([], theProp),
false,
'does not identify them as non-literal role elements',
);
t.equal(
isNonLiteralProperty([JSXAttributeMock(theProp, LiteralMock('theRole'))], theProp),
false,
'does not identify elements with a literal property as non-literal role elements without spread operator',
);
t.equal(
isNonLiteralProperty([spread, JSXAttributeMock(theProp, LiteralMock('theRole'))], theProp),
false,
'does not identify elements with a literal property as non-literal role elements with spread operator',
);
t.equal(
isNonLiteralProperty([JSXAttributeMock(theProp, JSXTextMock('theRole'))], theProp),
false,
'identifies elements with a JSXText property as non-literal role elements',
);
t.equal(
isNonLiteralProperty([JSXAttributeMock(theProp, IdentifierMock('undefined'))], theProp),
false,
'does not identify elements with a property of undefined as non-literal role elements',
);
t.equal(
isNonLiteralProperty([JSXAttributeMock(theProp, IdentifierMock('theIdentifier'))], theProp),
true,
'identifies elements with an expression property as non-literal role elements',
);
t.end();
});

View File

@@ -0,0 +1,72 @@
import test from 'tape';
import isSemanticRoleElement from '../../../src/util/isSemanticRoleElement';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('isSemanticRoleElement', (t) => {
t.equal(
isSemanticRoleElement('input', [
JSXAttributeMock('type', 'checkbox'),
JSXAttributeMock('role', 'switch'),
]),
true,
'identifies semantic role elements',
);
t.test('rejects non-semantics role elements', (st) => {
st.equal(
isSemanticRoleElement('input', [
JSXAttributeMock('type', 'radio'),
JSXAttributeMock('role', 'switch'),
]),
false,
);
st.equal(
isSemanticRoleElement('input', [
JSXAttributeMock('type', 'text'),
JSXAttributeMock('role', 'combobox'),
]),
false,
);
st.equal(
isSemanticRoleElement('button', [
JSXAttributeMock('role', 'switch'),
JSXAttributeMock('aria-pressed', 'true'),
]),
false,
);
st.equal(
isSemanticRoleElement('input', [
JSXAttributeMock('role', 'switch'),
]),
false,
);
st.end();
});
t.doesNotThrow(
() => {
isSemanticRoleElement('input', [
JSXAttributeMock('type', 'checkbox'),
JSXAttributeMock('role', 'checkbox'),
JSXAttributeMock('aria-checked', 'false'),
JSXAttributeMock('aria-labelledby', 'foo'),
JSXAttributeMock('tabindex', '0'),
{
type: 'JSXSpreadAttribute',
argument: {
type: 'Identifier',
name: 'props',
},
},
]);
},
'does not throw on JSXSpreadAttribute',
);
t.end();
});

View File

@@ -0,0 +1,219 @@
import test from 'tape';
import mayContainChildComponent from '../../../src/util/mayContainChildComponent';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import JSXExpressionContainerMock from '../../../__mocks__/JSXExpressionContainerMock';
test('mayContainChildComponent', (t) => {
t.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
]),
]),
]),
JSXElementMock('span', [], []),
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
]),
'FancyComponent',
5,
),
false,
'no FancyComponent returns false',
);
t.test('contains an indicated component', (st) => {
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('input'),
]),
'input',
),
true,
'returns true',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'FancyComponent',
),
true,
'returns true',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
]),
'FancyComponent',
),
false,
'FancyComponent is outside of default depth, should return false',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
]),
'FancyComponent',
2,
),
true,
'FancyComponent is inside of custom depth, should return true',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], [
JSXElementMock('span', [], [
JSXElementMock('FancyComponent'),
]),
]),
]),
]),
]),
JSXElementMock('span', [], []),
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
]),
'FancyComponent',
6,
),
true,
'deep nesting, returns true',
);
st.end();
});
t.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXExpressionContainerMock('mysteryBox'),
]),
'FancyComponent',
),
true,
'Intederminate situations + expression container children - returns true',
);
t.test('Glob name matching - component name contains question mark ? - match any single character', (st) => {
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'Fanc?Co??onent',
),
true,
'returns true',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'FancyComponent?',
),
false,
'returns false',
);
st.test('component name contains asterisk * - match zero or more characters', (s2t) => {
s2t.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'Fancy*',
),
true,
'returns true',
);
s2t.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'*Component',
),
true,
'returns true',
);
s2t.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'Fancy*C*t',
),
true,
'returns true',
);
s2t.end();
});
st.end();
});
t.test('using a custom elementType function', (st) => {
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('CustomInput'),
]),
'input',
2,
() => 'input',
),
true,
'returns true when the custom elementType returns the proper name',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('CustomInput'),
]),
'input',
2,
() => 'button',
),
false,
'returns false when the custom elementType returns a wrong name',
);
st.end();
});
t.end();
});

View File

@@ -0,0 +1,256 @@
import test from 'tape';
import mayHaveAccessibleLabel from '../../../src/util/mayHaveAccessibleLabel';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import JSXExpressionContainerMock from '../../../__mocks__/JSXExpressionContainerMock';
import JSXSpreadAttributeMock from '../../../__mocks__/JSXSpreadAttributeMock';
import JSXTextMock from '../../../__mocks__/JSXTextMock';
import LiteralMock from '../../../__mocks__/LiteralMock';
test('mayHaveAccessibleLabel', (t) => {
t.equal(
mayHaveAccessibleLabel(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
]),
]),
]),
JSXElementMock('span', [], []),
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
]),
5,
),
false,
'no label returns false',
);
t.test('label via attributes', (st) => {
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-label', 'A delicate label'),
], [])),
true,
'aria-label returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-label', ''),
], [])),
false,
'aria-label without content returns false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-label', ' '),
], [])),
false,
'aria-label with only spaces whitespace, should return false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-label', '\n'),
], [])),
false,
'aria-label with only newline whitespace, should return false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-labelledby', 'elementId'),
], [])),
true,
'aria-labelledby returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-labelledby', ''),
], [])),
false,
'aria-labelledby without content returns false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-labelledby', 'elementId', true),
], [])),
true,
'aria-labelledby with an expression container, should return true',
);
st.end();
});
t.test('label via custom label attribute', (st) => {
const customLabelProp = 'cowbell';
st.equal(
mayHaveAccessibleLabel(
JSXElementMock('div', [
JSXAttributeMock(customLabelProp, 'A delicate label'),
], []),
1,
[customLabelProp],
),
true,
'aria-label returns true',
);
st.end();
});
t.test('text label', (st) => {
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
LiteralMock('A fancy label'),
])),
true,
'Literal text, returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
LiteralMock(' '),
])),
false,
'Literal spaces whitespace, returns false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
LiteralMock('\n'),
])),
false,
'Literal newline whitespace, returns false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXTextMock('A fancy label'),
])),
true,
'JSXText, returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXTextMock('A fancy label'),
]),
])),
false,
'label is outside of default depth, returns false',
);
st.equal(
mayHaveAccessibleLabel(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXTextMock('A fancy label'),
]),
]),
2,
),
true,
'label is inside of custom depth, returns true',
);
st.equal(
mayHaveAccessibleLabel(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], [
JSXElementMock('span', [], [
JSXTextMock('A fancy label'),
]),
]),
]),
]),
]),
JSXElementMock('span', [], []),
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
]),
6,
),
true,
'deep nesting, returns true',
);
st.end();
});
t.test('image content', (st) => {
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
])),
false,
'without alt, returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
JSXAttributeMock('alt', 'A sensible label'),
]),
])),
true,
'with alt, returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
JSXAttributeMock('aria-label', 'A sensible label'),
]),
])),
true,
'with aria-label, returns true',
);
st.end();
});
t.test('Intederminate situations', (st) => {
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXExpressionContainerMock('mysteryBox'),
])),
true,
'expression container children, returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('style', 'some-junk'),
JSXSpreadAttributeMock('props'),
], [])),
true,
'spread operator in attributes, returns true',
);
st.end();
});
t.end();
});

View File

@@ -0,0 +1,93 @@
import { version as eslintVersion } from 'eslint/package.json';
import test from 'tape';
import semver from 'semver';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
const usingLegacy = semver.major(eslintVersion) < 9;
test('parserOptionsMapper', (t) => {
const expectedResult = usingLegacy
? {
code: '<div />',
errors: [],
options: {},
parserOptions: {
ecmaVersion: 2018,
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
},
settings: {},
}
: {
code: '<div />',
errors: [],
options: {},
languageOptions: {
ecmaVersion: 'latest',
parserOptions: {
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
},
},
settings: {},
};
t.deepEqual(
parserOptionsMapper({
code: '<div />',
errors: [],
options: {},
}),
expectedResult,
'returns a test case object',
);
const expectedResult2 = usingLegacy
? {
code: '<div />',
errors: [],
options: {},
parserOptions: {
ecmaVersion: 5,
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
},
settings: {},
}
: {
code: '<div />',
errors: [],
options: {},
languageOptions: {
ecmaVersion: 5,
parserOptions: {
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
},
},
settings: {},
};
t.deepEqual(
parserOptionsMapper({
code: '<div />',
errors: [],
options: {},
languageOptions: {
ecmaVersion: 5,
},
}),
expectedResult2,
'allows for overriding parserOptions',
);
t.end();
});

View File

@@ -0,0 +1,35 @@
import test from 'tape';
import { generateObjSchema, arraySchema, enumArraySchema } from '../../../src/util/schemas';
test('schemas', (t) => {
t.test('should generate an object schema with correct properties', (st) => {
const schema = generateObjSchema({
foo: 'bar',
baz: arraySchema,
});
const properties = schema.properties || {};
st.deepEqual(properties.foo, properties.foo, 'bar');
st.deepEqual(properties.baz.type, 'array');
st.end();
});
t.deepEqual(
enumArraySchema(),
{
additionalItems: false,
items: {
enum: [],
type: 'string',
},
minItems: 0,
type: 'array',
uniqueItems: true,
},
'enumArraySchema works with no arguments',
);
t.end();
});