Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/node-4+.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
matrix:
node-version: ${{ fromJson(needs.matrix.outputs.latest) }}
eslint:
- 10
- 9
- 8
- 7
Expand All @@ -33,6 +34,42 @@ jobs:
- 4
- 3
exclude:
- node-version: 23
eslint: 10
- node-version: 21
eslint: 10
- node-version: 19
eslint: 10
- node-version: 18
eslint: 10
- node-version: 17
eslint: 10
- node-version: 16
eslint: 10
- node-version: 15
eslint: 10
- node-version: 14
eslint: 10
- node-version: 13
eslint: 10
- node-version: 12
eslint: 10
- node-version: 11
eslint: 10
- node-version: 10
eslint: 10
- node-version: 9
eslint: 10
- node-version: 8
eslint: 10
- node-version: 7
eslint: 10
- node-version: 6
eslint: 10
- node-version: 5
eslint: 10
- node-version: 4
eslint: 10
- node-version: 16
eslint: 9
- node-version: 15
Expand Down
45 changes: 26 additions & 19 deletions __tests__/__util__/RuleTester.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import test from 'tape';
import mockProperty from 'mock-property';
import { RuleTester } from 'eslint';

const orig = RuleTester.prototype.run;
RuleTester.prototype.run = function (name, rule, tests) {
test(`RuleTester: ${name}`, (t) => {
t.teardown(mockProperty(RuleTester.describe, 't', { value: t }));
orig.call(this, name, rule, tests);
// Use only the documented RuleTester.describe and RuleTester.it hooks
// to integrate with tape. This avoids overriding RuleTester.prototype.run,
// which relies on undocumented internal stack-frame assumptions that
// changed in ESLint 10.

t.end();
});
};
let currentTest = null;

RuleTester.describe = function (text, method) {
RuleTester.it.title = text;
const self = this;
RuleTester.describe.t.test(text, (t) => {
t.teardown(mockProperty(RuleTester.it, 't', { value: t }));
method.call(self);
t.end();
});
RuleTester.describe = function describe(text, method) {
if (currentTest) {
// Nested describe (e.g. 'valid', 'invalid') — create a subtest
const parent = currentTest;
currentTest.test(text, (t) => {
currentTest = t;
method.call(this);
t.end();
currentTest = parent;
});
} else {
// Top-level describe (rule name) — create a new tape test
test(text, (t) => {
currentTest = t;
method.call(this);
t.end();
currentTest = null;
});
}
};

RuleTester.it = function (text, method) {
RuleTester.it.t.doesNotThrow(method, `${RuleTester.it.title}: ${text}`);
RuleTester.it = function it(text, method) {
currentTest.doesNotThrow(method, text);
};

export default RuleTester;
23 changes: 20 additions & 3 deletions __tests__/__util__/parserOptionsMapper.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { version as eslintVersion } from 'eslint/package.json';
import semver from 'semver';
import fromEntries from 'object.fromentries';
import entries from 'object.entries';

const usingLegacy = semver.major(eslintVersion) < 9;
const eslintMajor = semver.major(eslintVersion);
const usingLegacy = eslintMajor < 9;
const hasTestCaseErrorType = eslintMajor < 10;

const defaultParserOptions = {
ecmaFeatures: {
Expand All @@ -22,17 +26,30 @@ const defaultLanguageOptions = {
},
};

/**
* Strip the `type` property from error objects.
* ESLint v10 removed `TestCaseError#type`; including it causes test failures.
*/
function stripErrorType(err) {
if (typeof err === 'string') return err;
return fromEntries(entries(err).filter(([key]) => key !== 'type'));
}

export default function parserOptionsMapper({
code,
errors,
options = [],
languageOptions = {},
settings = {},
}) {
const mappedErrors = !hasTestCaseErrorType && Array.isArray(errors)
? errors.map(stripErrorType)
: errors;

return usingLegacy
? {
code,
errors,
errors: mappedErrors,
options,
parserOptions: {
...defaultLegacyParserOptions,
Expand All @@ -42,7 +59,7 @@ export default function parserOptionsMapper({
}
: {
code,
errors,
errors: mappedErrors,
options,
languageOptions: {
...defaultLanguageOptions,
Expand Down
111 changes: 111 additions & 0 deletions __tests__/eslint-compat-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* @fileoverview ESLint compatibility smoke tests.
*
* Verifies that the test infrastructure works with the currently-installed
* ESLint version. Catches regressions in the RuleTester/tape integration
* and version-conditional test-case behavior (e.g. error type stripping).
*/

import test from 'tape';
import { version as eslintVersion } from 'eslint/package.json';
import semver from 'semver';
import plugin from '../src';
import RuleTester from './__util__/RuleTester';
import parserOptionsMapper from './__util__/parserOptionsMapper';

const eslintMajor = semver.major(eslintVersion);

// ---------------------------------------------------------------------------
// RuleTester/tape integration smoke test
// ---------------------------------------------------------------------------
// Exercises the describe/it hooks that integrate RuleTester with tape.
// A failure here means the wrapper in __util__/RuleTester.js needs updating
// for the installed ESLint version.

const reportingRule = {
meta: {
type: 'suggestion',
schema: [{ type: 'object' }],
},
create(context) {
return {
JSXAttribute(node) {
if (node.name.name === 'accessKey') {
context.report({ node, message: 'accessKey is not allowed' });
}
},
};
},
};

const tester = new RuleTester();

// Run 1: valid + invalid — exercises full describe/it nesting
// (describe('rule') → describe('valid') → it, describe('invalid') → it)
tester.run('eslint-compat-smoke', reportingRule, {
valid: [
parserOptionsMapper({ code: '<div />;' }),
],
invalid: [
parserOptionsMapper({
code: '<div accessKey="h" />',
errors: [{ message: 'accessKey is not allowed', type: 'JSXAttribute' }],
}),
],
});

// Run 2: sequential run — verifies that the wrapper's currentTest
// state is properly reset between runs. If state leaks from run 1,
// this will crash or produce wrong results.
tester.run('eslint-compat-smoke-sequential', reportingRule, {
valid: [
parserOptionsMapper({ code: '<span />;' }),
parserOptionsMapper({ code: '<button />;' }),
],
invalid: [
parserOptionsMapper({
code: '<div accessKey="h" />',
errors: [{ message: 'accessKey is not allowed', type: 'JSXAttribute' }],
}),
],
});

// Run 3: same tester instance, third sequential run — exercises repeated
// reset of describe/it nesting to catch state accumulation bugs.
tester.run('eslint-compat-smoke-third', reportingRule, {
valid: [
parserOptionsMapper({ code: '<span />;' }),
parserOptionsMapper({ code: '<button />;' }),
parserOptionsMapper({ code: '<a />;' }),
],
invalid: [
parserOptionsMapper({
code: '<div accessKey="x" />',
errors: [{ message: 'accessKey is not allowed', type: 'JSXAttribute' }],
}),
parserOptionsMapper({
code: '<span accessKey="y" />',
errors: [{ message: 'accessKey is not allowed', type: 'JSXAttribute' }],
}),
],
});

// ---------------------------------------------------------------------------
// Config export shape
// ---------------------------------------------------------------------------

test(`ESLint ${eslintMajor}: flat config shape`, (t) => {
const { recommended: rec, strict } = plugin.flatConfigs;

t.ok(rec.plugins, 'recommended flat config has plugins');
t.ok(rec.rules, 'recommended flat config has rules');
t.ok(rec.languageOptions, 'recommended flat config has languageOptions');
t.equal(typeof rec.languageOptions.parserOptions, 'object', 'has parserOptions in languageOptions');
t.ok(rec.languageOptions.parserOptions.ecmaFeatures.jsx, 'jsx is enabled');

t.ok(strict.plugins, 'strict flat config has plugins');
t.ok(strict.rules, 'strict flat config has rules');
t.ok(strict.languageOptions, 'strict flat config has languageOptions');

t.end();
});
1 change: 0 additions & 1 deletion __tests__/src/rules/aria-unsupported-elements-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const ariaValidityTests = domElements.map((element) => {
};
}).concat({
code: '<fake aria-hidden />',
errors: [errorMessage('aria-hidden')],
});

// Generate invalid test cases.
Expand Down
13 changes: 4 additions & 9 deletions __tests__/src/rules/autocomplete-valid-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ const invalidAutocomplete = [{
type: 'JSXOpeningElement',
}];

const inappropriateAutocomplete = [{
message: axeFailMessage('autocomplete-appropriate'),
type: 'JSXOpeningElement',
}];

const componentsSettings = {
'jsx-a11y': {
components: {
Expand Down Expand Up @@ -60,10 +55,10 @@ ruleTester.run('autocomplete-valid', rule, {

// 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'] }] },
{ code: '<input type="date" autocomplete="email" />;' },
{ code: '<input type="number" autocomplete="url" />;' },
{ code: '<input type="month" autocomplete="tel" />;' },
{ code: '<Foo type="month" autocomplete="tel"></Foo>;', options: [{ inputComponents: ['Foo'] }] },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// FAILED "autocomplete-valid"
Expand Down
2 changes: 1 addition & 1 deletion __tests__/src/rules/html-has-lang-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ ruleTester.run('html-has-lang', rule, {
{ code: '<html lang={foo} />' },
{ code: '<html lang />' },
{ code: '<HTML />' },
{ code: '<HTMLTop lang="en" />', errors: [expectedError], settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } },
{ code: '<HTMLTop lang="en" />', settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<html />', errors: [expectedError] },
Expand Down
2 changes: 0 additions & 2 deletions __tests__/src/rules/no-noninteractive-tabindex-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,10 @@ ruleTester.run(`${ruleName}:recommended`, rule, {
{
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))
Expand Down
2 changes: 0 additions & 2 deletions __tests__/src/rules/no-static-element-interactions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,12 +442,10 @@ ruleTester.run(`${ruleName}:recommended`, rule, {
{
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))
Expand Down
21 changes: 20 additions & 1 deletion __tests__/src/util/parserOptionsMapper-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import semver from 'semver';

import parserOptionsMapper from '../../__util__/parserOptionsMapper';

const usingLegacy = semver.major(eslintVersion) < 9;
const eslintMajor = semver.major(eslintVersion);
const usingLegacy = eslintMajor < 9;
const hasTestCaseErrorType = eslintMajor < 10;

test('parserOptionsMapper', (t) => {
const expectedResult = usingLegacy
Expand Down Expand Up @@ -89,5 +91,22 @@ test('parserOptionsMapper', (t) => {
'allows for overriding parserOptions',
);

const errorsWithType = [{ message: 'bad', type: 'JSXOpeningElement' }];
const expectedErrors = hasTestCaseErrorType
? errorsWithType
: [{ message: 'bad' }];
const resultWithErrors = parserOptionsMapper({
code: '<div />',
errors: errorsWithType,
options: {},
});
t.deepEqual(
resultWithErrors.errors,
expectedErrors,
hasTestCaseErrorType
? 'preserves error type for eslint < 10'
: 'strips error type for eslint >= 10',
);

t.end();
});
Loading