Skip to content

Commit

Permalink
chore(prefer-spy-on): migrate to TS (#326)
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath authored and SimenB committed Jul 22, 2019
1 parent 3a22ef1 commit 4200e76
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 86 deletions.
@@ -1,7 +1,10 @@
import { RuleTester } from 'eslint';
import {
AST_NODE_TYPES,
TSESLint,
} from '@typescript-eslint/experimental-utils';
import rule from '../prefer-spy-on';

const ruleTester = new RuleTester({
const ruleTester = new TSESLint.RuleTester({
parserOptions: {
ecmaVersion: 6,
},
Expand All @@ -22,44 +25,84 @@ ruleTester.run('prefer-spy-on', rule, {
invalid: [
{
code: 'obj.a = jest.fn(); const test = 10;',
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
errors: [
{
messageId: 'useJestSpyOn',
type: AST_NODE_TYPES.AssignmentExpression,
},
],
output: "jest.spyOn(obj, 'a'); const test = 10;",
},
{
code: "Date['now'] = jest['fn']()",
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
errors: [
{
messageId: 'useJestSpyOn',
type: AST_NODE_TYPES.AssignmentExpression,
},
],
output: "jest.spyOn(Date, 'now')",
},
{
code: 'window[`${name}`] = jest[`fn`]()',
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
errors: [
{
messageId: 'useJestSpyOn',
type: AST_NODE_TYPES.AssignmentExpression,
},
],
output: 'jest.spyOn(window, `${name}`)',
},
{
code: "obj['prop' + 1] = jest['fn']()",
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
errors: [
{
messageId: 'useJestSpyOn',
type: AST_NODE_TYPES.AssignmentExpression,
},
],
output: "jest.spyOn(obj, 'prop' + 1)",
},
{
code: 'obj.one.two = jest.fn(); const test = 10;',
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
errors: [
{
messageId: 'useJestSpyOn',
type: AST_NODE_TYPES.AssignmentExpression,
},
],
output: "jest.spyOn(obj.one, 'two'); const test = 10;",
},
{
code: 'obj.a = jest.fn(() => 10)',
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
errors: [
{
messageId: 'useJestSpyOn',
type: AST_NODE_TYPES.AssignmentExpression,
},
],
output: "jest.spyOn(obj, 'a').mockImplementation(() => 10)",
},
{
code:
"obj.a.b = jest.fn(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();",
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
errors: [
{
messageId: 'useJestSpyOn',
type: AST_NODE_TYPES.AssignmentExpression,
},
],
output:
"jest.spyOn(obj.a, 'b').mockImplementation(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();",
},
{
code: 'window.fetch = jest.fn(() => ({})).one.two().three().four',
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
errors: [
{
messageId: 'useJestSpyOn',
type: AST_NODE_TYPES.AssignmentExpression,
},
],
output:
"jest.spyOn(window, 'fetch').mockImplementation(() => ({})).one.two().three().four",
},
Expand Down
71 changes: 0 additions & 71 deletions src/rules/prefer-spy-on.js

This file was deleted.

100 changes: 100 additions & 0 deletions src/rules/prefer-spy-on.ts
@@ -0,0 +1,100 @@
import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import { createRule, getNodeName } from './tsUtils';

const findNodeObject = (
node: TSESTree.CallExpression | TSESTree.MemberExpression,
): TSESTree.LeftHandSideExpression | null => {
if ('object' in node) {
return node.object;
}

if (node.callee.type === AST_NODE_TYPES.MemberExpression) {
return node.callee.object;
}

return null;
};

const getJestFnCall = (node: TSESTree.Node): TSESTree.CallExpression | null => {
if (
node.type !== AST_NODE_TYPES.CallExpression &&
node.type !== AST_NODE_TYPES.MemberExpression
) {
return null;
}

const obj = findNodeObject(node);

if (!obj) {
return null;
}

if (obj.type === AST_NODE_TYPES.Identifier) {
return node.type === AST_NODE_TYPES.CallExpression &&
getNodeName(node.callee) === 'jest.fn'
? node
: null;
}

return getJestFnCall(obj);
};

export default createRule({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `jest.spyOn()`',
recommended: false,
},
messages: {
useJestSpyOn: 'Use jest.spyOn() instead.',
},
fixable: 'code',
schema: [],
type: 'suggestion',
},
defaultOptions: [],
create(context) {
return {
AssignmentExpression(node) {
const { left, right } = node;

if (left.type !== AST_NODE_TYPES.MemberExpression) return;

const jestFnCall = getJestFnCall(right);

if (!jestFnCall) return;

context.report({
node,
messageId: 'useJestSpyOn',
fix(fixer) {
const leftPropQuote =
left.property.type === AST_NODE_TYPES.Identifier ? "'" : '';
const [arg] = jestFnCall.arguments;
const argSource = arg && context.getSourceCode().getText(arg);
const mockImplementation = argSource
? `.mockImplementation(${argSource})`
: '';

return [
fixer.insertTextBefore(left, `jest.spyOn(`),
fixer.replaceTextRange(
[left.object.range[1], left.property.range[0]],
`, ${leftPropQuote}`,
),
fixer.replaceTextRange(
[left.property.range[1], jestFnCall.range[1]],
`${leftPropQuote})${mockImplementation}`,
),
];
},
});
},
};
},
});
5 changes: 0 additions & 5 deletions src/rules/util.js
Expand Up @@ -109,11 +109,6 @@ export const getNodeName = node => {
switch (node && node.type) {
case 'Identifier':
return node.name;
case 'Literal':
return node.value;
case 'TemplateLiteral':
if (node.expressions.length === 0) return node.quasis[0].value.cooked;
break;
case 'MemberExpression':
return joinNames(getNodeName(node.object), getNodeName(node.property));
}
Expand Down

0 comments on commit 4200e76

Please sign in to comment.