Skip to content

Commit

Permalink
fix(prefer-expect-assertions): support .each (#798)
Browse files Browse the repository at this point in the history
Fixes #676
  • Loading branch information
G-Rath authored Apr 2, 2021
1 parent 243cb4f commit f758243
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 21 deletions.
1 change: 1 addition & 0 deletions src/rules/__tests__/lowercase-name.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const ruleTester = new TSESLint.RuleTester({
ruleTester.run('lowercase-name', rule, {
valid: [
'it.each()',
'it.each()(1)',
'randomFunction()',
'foo.bar()',
'it()',
Expand Down
181 changes: 181 additions & 0 deletions src/rules/__tests__/prefer-expect-assertions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const ruleTester = new TSESLint.RuleTester({

ruleTester.run('prefer-expect-assertions', rule, {
valid: [
'test("nonsense", [])',
'test("it1", () => {expect.assertions(0);})',
'test("it1", function() {expect.assertions(0);})',
'test("it1", function() {expect.hasAssertions();})',
Expand Down Expand Up @@ -259,3 +260,183 @@ ruleTester.run('prefer-expect-assertions', rule, {
},
],
});

ruleTester.run('.each support', rule, {
valid: [
'test.each()("is fine", () => { expect.assertions(0); })',
'test.each``("is fine", () => { expect.assertions(0); })',
'test.each()("is fine", () => { expect.hasAssertions(); })',
'test.each``("is fine", () => { expect.hasAssertions(); })',
'it.each()("is fine", () => { expect.assertions(0); })',
'it.each``("is fine", () => { expect.assertions(0); })',
'it.each()("is fine", () => { expect.hasAssertions(); })',
'it.each``("is fine", () => { expect.hasAssertions(); })',
{
code: 'test.each()("is fine", () => {})',
options: [{ onlyFunctionsWithAsyncKeyword: true }],
},
{
code: 'test.each``("is fine", () => {})',
options: [{ onlyFunctionsWithAsyncKeyword: true }],
},
{
code: 'it.each()("is fine", () => {})',
options: [{ onlyFunctionsWithAsyncKeyword: true }],
},
{
code: 'it.each``("is fine", () => {})',
options: [{ onlyFunctionsWithAsyncKeyword: true }],
},
dedent`
describe.each(['hello'])('%s', () => {
it('is fine', () => {
expect.assertions(0);
});
});
`,
dedent`
describe.each\`\`('%s', () => {
it('is fine', () => {
expect.assertions(0);
});
});
`,
dedent`
describe.each(['hello'])('%s', () => {
it('is fine', () => {
expect.hasAssertions();
});
});
`,
dedent`
describe.each\`\`('%s', () => {
it('is fine', () => {
expect.hasAssertions();
});
});
`,
dedent`
describe.each(['hello'])('%s', () => {
it.each()('is fine', () => {
expect.assertions(0);
});
});
`,
dedent`
describe.each\`\`('%s', () => {
it.each()('is fine', () => {
expect.assertions(0);
});
});
`,
dedent`
describe.each(['hello'])('%s', () => {
it.each()('is fine', () => {
expect.hasAssertions();
});
});
`,
dedent`
describe.each\`\`('%s', () => {
it.each()('is fine', () => {
expect.hasAssertions();
});
});
`,
],
invalid: [
{
code: dedent`
test.each()("is not fine", () => {
expect(someValue).toBe(true);
});
`,
errors: [
{
messageId: 'haveExpectAssertions',
column: 1,
line: 1,
},
],
},
{
code: dedent`
describe.each()('something', () => {
it("is not fine", () => {
expect(someValue).toBe(true);
});
});
`,
errors: [
{
messageId: 'haveExpectAssertions',
column: 3,
line: 2,
},
],
},
{
code: dedent`
describe.each()('something', () => {
test.each()("is not fine", () => {
expect(someValue).toBe(true);
});
});
`,
errors: [
{
messageId: 'haveExpectAssertions',
column: 3,
line: 2,
},
],
},
{
code: dedent`
test.each()("is not fine", async () => {
expect(someValue).toBe(true);
});
`,
options: [{ onlyFunctionsWithAsyncKeyword: true }],
errors: [
{
messageId: 'haveExpectAssertions',
column: 1,
line: 1,
},
],
},
{
code: dedent`
it.each()("is not fine", async () => {
expect(someValue).toBe(true);
});
`,
options: [{ onlyFunctionsWithAsyncKeyword: true }],
errors: [
{
messageId: 'haveExpectAssertions',
column: 1,
line: 1,
},
],
},
{
code: dedent`
describe.each()('something', () => {
test.each()("is not fine", async () => {
expect(someValue).toBe(true);
});
});
`,
options: [{ onlyFunctionsWithAsyncKeyword: true }],
errors: [
{
messageId: 'haveExpectAssertions',
column: 3,
line: 2,
},
],
},
],
});
4 changes: 2 additions & 2 deletions src/rules/lowercase-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ const findNodeNameAndArgument = (

if (isEachCall(node)) {
if (
node.parent?.type === AST_NODE_TYPES.CallExpression &&
hasStringAsFirstArgument(node.parent)
node.parent.arguments.length > 0 &&
isStringNode(node.parent.arguments[0])
) {
return [node.callee.object.name, node.parent.arguments[0]];
}
Expand Down
38 changes: 23 additions & 15 deletions src/rules/prefer-expect-assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
createRule,
getAccessorValue,
hasOnlyOneArgument,
isEachCall,
isFunction,
isSupportedAccessor,
isTestCase,
} from './utils';

const isExpectAssertionsOrHasAssertionsCall = (
Expand Down Expand Up @@ -40,12 +43,6 @@ const suggestRemovingExtraArguments = (
]),
});

interface PreferExpectAssertionsCallExpression extends TSESTree.CallExpression {
arguments: [
TSESTree.Expression,
TSESTree.ArrowFunctionExpression & { body: TSESTree.BlockStatement },
];
}
interface RuleOptions {
onlyFunctionsWithAsyncKeyword?: boolean;
}
Expand Down Expand Up @@ -103,14 +100,28 @@ export default createRule<[RuleOptions], MessageIds>({
defaultOptions: [{ onlyFunctionsWithAsyncKeyword: false }],
create(context, [options]) {
return {
'CallExpression[callee.name=/^(it|test)$/][arguments.1.body.body]'(
node: PreferExpectAssertionsCallExpression,
) {
if (options.onlyFunctionsWithAsyncKeyword && !node.arguments[1].async) {
CallExpression(node: TSESTree.CallExpression) {
if (!isTestCase(node)) {
return;
}

const args = isEachCall(node) ? node.parent.arguments : node.arguments;

if (args.length < 2) {
return;
}

const [, testFn] = args;

if (
!isFunction(testFn) ||
testFn.body.type !== AST_NODE_TYPES.BlockStatement ||
(options.onlyFunctionsWithAsyncKeyword && !testFn.async)
) {
return;
}

const testFuncBody = node.arguments[1].body.body;
const testFuncBody = testFn.body.body;

if (!isFirstLineExprStmt(testFuncBody)) {
context.report({
Expand All @@ -120,10 +131,7 @@ export default createRule<[RuleOptions], MessageIds>({
messageId,
fix: fixer =>
fixer.insertTextBeforeRange(
[
node.arguments[1].body.range[0] + 1,
node.arguments[1].body.range[1],
],
[testFn.body.range[0] + 1, testFn.body.range[1]],
text,
),
})),
Expand Down
9 changes: 5 additions & 4 deletions src/rules/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,19 +692,20 @@ export const isDescribe = (
DescribeAlias.hasOwnProperty(node.callee.tag.object.name));

/**
* Checks if the given node` is a call to `<describe|test|it>.each(...)`.
* If `true`, the code must look like `<method>.each(...)`.
* Checks if the given node` is a call to `<describe|test|it>.each(...)()`.
* If `true`, the code must look like `<method>.each(...)()`.
*
* @param {JestFunctionCallExpression<DescribeAlias | TestCaseName>} node
*
* @return {node is JestFunctionCallExpressionWithMemberExpressionCallee<DescribeAlias | TestCaseName, DescribeProperty.each | TestCaseProperty.each>}
* @return {node is JestFunctionCallExpressionWithMemberExpressionCallee<DescribeAlias | TestCaseName, DescribeProperty.each | TestCaseProperty.each> & {parent: TSESTree.CallExpression}}
*/
export const isEachCall = (
node: JestFunctionCallExpression<DescribeAlias | TestCaseName>,
): node is JestFunctionCallExpressionWithMemberExpressionCallee<
DescribeAlias | TestCaseName,
DescribeProperty.each | TestCaseProperty.each
> =>
> & { parent: TSESTree.CallExpression } =>
node.parent?.type === AST_NODE_TYPES.CallExpression &&
node.callee.type === AST_NODE_TYPES.MemberExpression &&
isSupportedAccessor(node.callee.property, DescribeProperty.each);

Expand Down

0 comments on commit f758243

Please sign in to comment.