Skip to content

Commit

Permalink
add no-deprecated-selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
snoozbuster committed Jun 8, 2022
1 parent 762feaf commit cf79b97
Show file tree
Hide file tree
Showing 5 changed files with 433 additions and 0 deletions.
83 changes: 83 additions & 0 deletions docs/rules/no-deprecated-selectors.md
@@ -0,0 +1,83 @@
# Checks that Wrapper methods are called with appropriate selectors. (vue-test-utils/no-deprecated-selectors)

The `--fix` option on the command line can automatically fix some of the problems reported by this rule.

## Rule Details

This rule reports `Wrapper` `find*` and `get*` calls which are using improper selectors for their return types. For example, `find` should be called with a CSS selector and should be expected to return a DOM element, and `findComponent` should be called with a component selector and should be expected to return a Vue component.

Addiitonally, this rule reports `wrapper.vm` usages which are chained off an improper selector function. For example, `wrapper.find('div')` always returns a DOM element in VTU 2, making `wrapper.find('div').vm` an incorrect usage.

### Options

This rule has an object option:

- `wrapperNames` can be set to an array of variable names that are checked for deprecated function calls.

Examples of **incorrect** code for this rule:

```js
/* eslint vue-test-utils/no-deprecated-selectors: "error" */
import MyComponent from './MyComponent.vue';

const wrapper = mount(MyComponent);

wrapper.get('div').vm.$emit('click');
wrapper.get(MyComponent).setProps(/* ... */);
expect(wrapper.findAll(FooComponent)).at(0)).toBeTruthy();
```

Examples of **correct** code for this rule:

```js
/* eslint vue-test-utils/no-deprecated-selectors: "error" */
import MyComponent from './MyComponent.vue';

const wrapper = mount(MyComponent);

wrapper.getComponent(DivComponent).vm.$emit('click');
wrapper.getComponent(MyComponent).setProps(/* ... */);
expect(wrapper.findAllComponents(FooComponent).at(0)).toBeTruthy();
```

Examples of **incorrect** code with the `{ "wrapperName": ["component"] }` option:

```js
/* eslint vue-test-utils/no-deprecated-selectors: ["error", { "wrapperName": ["component"] }] */
import MyComponent from './MyComponent.vue';

const component = mount(MyComponent);

component.get('div').vm.$emit('click');
component.get(MyComponent).setProps(/* ... */);
expect(component.findAll(FooComponent).at(0)).toBeTruthy();
```

Examples of **correct** code with the `{ "wrapperName": ["component"] }` option:

```js
/* eslint vue-test-utils/no-deprecated-selectors: ["error", { "wrapperName": ["component"] }] */
import MyComponent from './MyComponent.vue';

const component = mount(MyComponent);

component.getComponent(DivComponent).vm.$emit('click');
component.getComponent(MyComponent).setProps(/* ... */);

const wrapper = mount(MyComponent);

// not reported because `wrapper` is not in the list of `wrapperName`s
wrapper.get(MyComponent).vm.$emit('click');
```

## Limitations

- This rule cannot detect wrappers if they are not stored into a local variable with a name matching one of the names in the `wrapperNames` option (eg, `mount(Foo).get(MyComponent)` will never error)

## When Not To Use It

- Never

## Further Reading

- [VTU 1 docs](https://vue-test-utils.vuejs.org/api/wrapper/#find)
3 changes: 3 additions & 0 deletions src/index.js
@@ -1,4 +1,5 @@
const noDeprecatedMountOptions = require('./rules/no-deprecated-mount-options');
const noDeprecatedSelectors = require('./rules/no-deprecated-selectors');
const noDeprecatedWrappers = require('./rules/no-deprecated-wrapper-functions');

//------------------------------------------------------------------------------
Expand All @@ -7,6 +8,7 @@ const noDeprecatedWrappers = require('./rules/no-deprecated-wrapper-functions');

module.exports.rules = {
'no-deprecated-mount-options': noDeprecatedMountOptions,
'no-deprecated-selectors': noDeprecatedSelectors,
'no-deprecated-wrapper-functions': noDeprecatedWrappers,
};

Expand All @@ -15,6 +17,7 @@ module.exports.configs = {
plugins: ['vue-test-utils'],
rules: {
'vue-test-utils/no-deprecated-mount-options': 'error',
'vue-test-utils/no-deprecated-selectors': 'error',
'vue-test-utils/no-deprecated-wrapper-functions': 'error',
},
},
Expand Down
137 changes: 137 additions & 0 deletions src/rules/no-deprecated-selectors.js
@@ -0,0 +1,137 @@
const { get } = require('lodash');
const path = require('path');
const isVtuVersionAtLeast = require('./checkVtuVersion');
const { VTU_PLUGIN_SETTINGS_KEY } = require('./constants');
const { nodeIsCalledFromWrapper, nodeCalleeReturnsWrapper, isComponentSelector } = require('./utils');
const { detectVtuVersion } = isVtuVersionAtLeast;

const DEFAULT_WRAPPER_VARIABLES = ['wrapper'];

/**
* @type {import('eslint').Rule.RuleModule}
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow deprecated selector usage',
url: path.join(__dirname, '../../docs/rules/no-deprecated-selectors.md'),
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
wrapperNames: {
description: 'List of variable names to which wrappers are typically assigned',
type: 'array',
items: {
type: 'string',
},
},
},
},
], // Add a schema if the rule has options
messages: {
deprecatedComponentSelector:
'Calling {{ functionName }} with a component selector is deprecated and will be removed in VTU 2.',
memberUsageFromDeprecatedSelector:
'{{ functionName }} will no longer return `wrapper.{{ missingMemberName }}` in VTU 2. Use {{ alternateFunctionName }} with a component selector instead.',
},
},

create(context) {
const wrapperNames = (context.options[0] && context.options[0].wrapperNames) || DEFAULT_WRAPPER_VARIABLES;
const vtuVersion = get(context.settings, [VTU_PLUGIN_SETTINGS_KEY, 'version']) || detectVtuVersion();

const componentOnlyWrapperMembers = new Set(['vm', 'props', 'setData', 'setProps', 'emitted']);

const deprecatedComponentSelectorFunctions = {
// functionName => preferred name
find: 'findComponent',
findAll: 'findAllComponents',
get: 'getComponent',
};

const canChainComponentsFromCssWrappers = isVtuVersionAtLeast(vtuVersion, '1.3.0');

return {
CallExpression(node) {
if (node.callee.type !== 'MemberExpression' || node.callee.property.type !== 'Identifier') {
return;
}

if (
node.callee.property.name in deprecatedComponentSelectorFunctions &&
nodeIsCalledFromWrapper(node.callee.object, wrapperNames)
) {
// these functions should always have strings passed to them, never objects or components
if (node.arguments[0] && isComponentSelector(node.arguments[0], context)) {
let isSuccessiveWrapperChain = false;
let wrapperSelectorCall = node.callee.object;
while (nodeCalleeReturnsWrapper(wrapperSelectorCall)) {
if (wrapperSelectorCall.callee.property.name in deprecatedComponentSelectorFunctions) {
// cannot autofix in versions before 1.3 because this is a chain like `get('div').get(SomeComponent)`.
// Autofixing to `get('div').getComponent(SomeComponent)` will cause an error on those versions
isSuccessiveWrapperChain = true;
break;
}
wrapperSelectorCall = wrapperSelectorCall.callee.object;
}

context.report({
messageId: 'deprecatedComponentSelector',
node: node.arguments[0],
data: {
functionName: node.callee.property.name,
},
fix:
(isSuccessiveWrapperChain && !canChainComponentsFromCssWrappers)
? undefined
: fixer => {
return fixer.replaceText(
node.callee.property,
deprecatedComponentSelectorFunctions[node.callee.property.name]
);
},
});
return;
}
}
},
MemberExpression(node) {
if (
node.property.type === 'Identifier' &&
componentOnlyWrapperMembers.has(node.property.name) &&
nodeIsCalledFromWrapper(node.object, wrapperNames) &&
nodeCalleeReturnsWrapper(node.object) // if object isn't a call which returns wrapper, then member usage is rooted directly off wrapper which is safe
) {
// the member usage should be not be chained immediately after a non-component selector function
// (okay if previous calls don't return components as long as the last one does)
let lastWrapperCall = node.object;
if (
lastWrapperCall.callee.property.name === 'at' &&
nodeCalleeReturnsWrapper(lastWrapperCall.callee.object)
) {
// special handling for findAll().at().foo - need to make sure we're looking at the 'findAll',
// not the 'at'
lastWrapperCall = lastWrapperCall.callee.object;
}
if (lastWrapperCall.callee.property.name in deprecatedComponentSelectorFunctions) {
context.report({
messageId: 'memberUsageFromDeprecatedSelector',
node,
data: {
functionName: lastWrapperCall.callee.property.name,
missingMemberName: node.property.name,
alternateFunctionName:
deprecatedComponentSelectorFunctions[lastWrapperCall.callee.property.name],
},
});
return;
}
}
},
};
},
};
43 changes: 43 additions & 0 deletions src/rules/utils.js
Expand Up @@ -76,6 +76,48 @@ function getImportSourceName(boundIdentifier) {
return importDefinition.node.parent.source.value;
}

/**
*
* @param {*} node
* @param {import('eslint').Rule.RuleContext} context
* @returns
*/
function isComponentSelector(node, context) {
if (node.type === 'ObjectExpression') {
return true;
}

const boundIdentifier = resolveIdentifierToVariable(node, context.getScope());
if (!boundIdentifier) {
return false;
}

const importDefinition = boundIdentifier.defs.find(({ type }) => type === 'ImportBinding');
if (importDefinition) {
const importedName =
importDefinition.node.type === 'ImportSpecifier'
? importDefinition.node.imported.name
: /* default import */ undefined;
const importSourceName = getImportSourceName(boundIdentifier);

const isVueSourceFileImport = importSourceName.endsWith('.vue');

// short circuit to avoid costly module resolution attempts
return (
isVueSourceFileImport ||
isComponentImport(
importSourceName,
importedName,
// <text> is a special value indicating the input came from stdin
context.getFilename() === '<text>' ? context.getCwd() : context.getFilename()
)
);
}

// note(@alexv): could potentially add logic here to check for object literal assignment, require(), or mount()
return false;
}

function isVtuImport(identifierNode, scope) {
const boundIdentifier = resolveIdentifierToVariable(identifierNode, scope);
if (!boundIdentifier) {
Expand All @@ -87,5 +129,6 @@ function isVtuImport(identifierNode, scope) {
module.exports = {
nodeCalleeReturnsWrapper,
nodeIsCalledFromWrapper,
isComponentSelector,
isVtuImport,
};

0 comments on commit cf79b97

Please sign in to comment.