Skip to content

Commit

Permalink
[Tests] migrate helper parsers function from eslint-plugin-react
Browse files Browse the repository at this point in the history
Co-authored-by: Ross Rosales <52366381+ROSSROSALES@users.noreply.github.com>
Co-authored-by: Jordan Harband <ljharb@gmail.com>
  • Loading branch information
ROSSROSALES and ljharb committed Oct 11, 2022
1 parent 9688ad8 commit ce4d57f
Show file tree
Hide file tree
Showing 42 changed files with 534 additions and 280 deletions.
9 changes: 9 additions & 0 deletions __tests__/__util__/helpers/getESLintCoreRule.js
@@ -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;
186 changes: 186 additions & 0 deletions __tests__/__util__/helpers/parsers.js
@@ -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') })

Check warning on line 14 in __tests__/__util__/helpers/parsers.js

View check run for this annotation

Codecov / codecov/patch

__tests__/__util__/helpers/parsers.js#L14

Added line #L14 was not covered by tests
: (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 };

Check warning on line 75 in __tests__/__util__/helpers/parsers.js

View check run for this annotation

Codecov / codecov/patch

__tests__/__util__/helpers/parsers.js#L75

Added line #L75 was not covered by tests
}
if ('parser' in test) {
delete test.features;
return test;

Check warning on line 79 in __tests__/__util__/helpers/parsers.js

View check run for this annotation

Codecov / codecov/patch

__tests__/__util__/helpers/parsers.js#L78-L79

Added lines #L78 - L79 were not covered by tests
}
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 })),

Check warning on line 110 in __tests__/__util__/helpers/parsers.js

View check run for this annotation

Codecov / codecov/patch

__tests__/__util__/helpers/parsers.js#L110

Added line #L110 was not covered by tests
};

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;
9 changes: 5 additions & 4 deletions __tests__/src/rules/accessible-emoji-test.js
Expand Up @@ -10,6 +10,7 @@
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/accessible-emoji';
import parsers from '../../__util__/helpers/parsers';

// -----------------------------------------------------------------------------
// Tests
Expand All @@ -23,7 +24,7 @@ const expectedError = {
};

ruleTester.run('accessible-emoji', rule, {
valid: [
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<span />' },
{ code: '<span>No emoji here!</span>' },
Expand All @@ -42,8 +43,8 @@ ruleTester.run('accessible-emoji', rule, {
code: '<CustomInput type="hidden">🐼</CustomInput>',
settings: { 'jsx-a11y': { components: { CustomInput: 'input' } } },
},
].map(parserOptionsMapper),
invalid: [
)).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] },
Expand All @@ -52,5 +53,5 @@ ruleTester.run('accessible-emoji', rule, {
{ code: '<Foo>🐼</Foo>', errors: [expectedError] },
{ code: '<span aria-hidden="false">🐼</span>', errors: [expectedError] },
{ code: '<CustomInput type="hidden">🐼</CustomInput>', errors: [expectedError] },
].map(parserOptionsMapper),
)).map(parserOptionsMapper),
});
29 changes: 20 additions & 9 deletions __tests__/src/rules/alt-text-test.js
Expand Up @@ -9,6 +9,7 @@

import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/alt-text';

// -----------------------------------------------------------------------------
Expand All @@ -28,19 +29,29 @@ Use alt="" for presentational images.`,
type: 'JSXOpeningElement',
});

const ariaLabelValueError = 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.';
const ariaLabelledbyValueError = 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.';
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 = 'Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.';
const objectError = {
message: 'Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.',
};

const areaError = 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.';
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 = '<input> elements with type="image" 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': {
Expand All @@ -58,7 +69,7 @@ const array = [{
}];

ruleTester.run('alt-text', rule, {
valid: [
valid: parsers.all([].concat(
// DEFAULT ELEMENT 'img' TESTS
{ code: '<img alt="foo" />;' },
{ code: '<img alt={"foo"} />;' },
Expand Down Expand Up @@ -166,8 +177,8 @@ ruleTester.run('alt-text', rule, {
{ code: '<InputImage alt="" />', options: array },
{ code: '<InputImage alt="This is descriptive!" />', options: array },
{ code: '<InputImage alt={altText} />', options: array },
].map(parserOptionsMapper),
invalid: [
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// DEFAULT ELEMENT 'img' TESTS
{ code: '<img />;', errors: [missingPropError('img')] },
{ code: '<img alt />;', errors: [altValueError('img')] },
Expand Down Expand Up @@ -273,5 +284,5 @@ ruleTester.run('alt-text', rule, {
{ 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),
)).map(parserOptionsMapper),
});
9 changes: 5 additions & 4 deletions __tests__/src/rules/anchor-ambiguous-text-test.js
Expand Up @@ -10,6 +10,7 @@

import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/anchor-ambiguous-text';

// -----------------------------------------------------------------------------
Expand All @@ -34,7 +35,7 @@ const expectedErrorGenerator = (words) => ({
const expectedError = expectedErrorGenerator(DEFAULT_AMBIGUOUS_WORDS);

ruleTester.run('anchor-ambiguous-text', rule, {
valid: [
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>;' },
Expand Down Expand Up @@ -69,8 +70,8 @@ ruleTester.run('anchor-ambiguous-text', rule, {
}],
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
].map(parserOptionsMapper),
invalid: [
)).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] },
Expand Down Expand Up @@ -113,5 +114,5 @@ ruleTester.run('anchor-ambiguous-text', rule, {
words: ['a disallowed word'],
}],
},
].map(parserOptionsMapper),
)).map(parserOptionsMapper),
});
9 changes: 5 additions & 4 deletions __tests__/src/rules/anchor-has-content-test.js
Expand Up @@ -9,6 +9,7 @@

import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/anchor-has-content';

// -----------------------------------------------------------------------------
Expand All @@ -23,7 +24,7 @@ const expectedError = {
};

ruleTester.run('anchor-has-content', rule, {
valid: [
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<a>Foo</a>' },
{ code: '<a><Bar /></a>' },
Expand All @@ -39,8 +40,8 @@ ruleTester.run('anchor-has-content', rule, {
{ code: '<a title={title} />' },
{ code: '<a aria-label={ariaLabel} />' },
{ code: '<a title={title} aria-label={ariaLabel} />' },
].map(parserOptionsMapper),
invalid: [
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<a />', errors: [expectedError] },
{ code: '<a><Bar aria-hidden /></a>', errors: [expectedError] },
{ code: '<a>{undefined}</a>', errors: [expectedError] },
Expand All @@ -49,5 +50,5 @@ ruleTester.run('anchor-has-content', rule, {
errors: [expectedError],
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
].map(parserOptionsMapper),
)).map(parserOptionsMapper),
});

0 comments on commit ce4d57f

Please sign in to comment.