Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f335463
commit 762feaf
Showing
7 changed files
with
543 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# Checks that mount and shallowMount options are valid for the current version of VTU. (vue-test-utils/no-deprecated-mount-options) | ||
|
||
The `--fix` option on the command line can automatically fix some of the problems reported by this rule. | ||
|
||
## Rule Details | ||
|
||
This rule reports when mount options that are deprecated or unsupported in the current version of VTU are used. | ||
|
||
### Options | ||
|
||
This rule has an object option: | ||
|
||
- `ignoreMountOptions` can be set to an array of property names that are ignored when checking for deprecated mount options. | ||
- This option is primarily useful when using a compatibility layer for Vue 3 or VTU 2 such as `@vue/compat` or `vue-test-utils-compat`. | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```js | ||
/* eslint vue-test-utils/no-deprecated-mount-options: "error" */ | ||
import { mount } from '@vue/test-utils'; | ||
|
||
// VTU 1 | ||
mount(MyComponent, { | ||
attachToDocument: true, | ||
}); | ||
|
||
mount(MyComponent, { | ||
computed: { /* ... */ } | ||
methods: { /* ... */ }, | ||
}); | ||
|
||
// VTU 2 | ||
mount(MyComponent, { | ||
propsData: { /* ... */ } | ||
}) | ||
|
||
mount(MyComponent, { | ||
stubs: [/* ... */] | ||
}) | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```js | ||
/* eslint vue-test-utils/no-deprecated-mount-options: "error" */ | ||
import { mount } from '@vue/test-utils'; | ||
|
||
// VTU 1 | ||
mount(MyComponent, { | ||
attachTo: document.body, | ||
}); | ||
|
||
mount(MyComponent, { | ||
mixins: [ | ||
/* ... */ | ||
], | ||
}); | ||
|
||
// VTU 2 | ||
mount(MyComponent, { | ||
props: { | ||
/* ... */ | ||
}, | ||
}); | ||
|
||
mount(MyComponent, { | ||
global: { | ||
stubs: [ | ||
/* ... */ | ||
], | ||
}, | ||
}); | ||
``` | ||
|
||
Examples of **correct** code with the `{ "ignoreMountOptions": ["store", "scopedSlots"] }` option: | ||
|
||
```js | ||
/* eslint vue-test-utils/no-deprecated-mount-options: ["error", { "ignoreMountOptions": ["store", "scopedSlots"] }] */ | ||
import { mount } from '@vue/test-utils'; | ||
|
||
// VTU 2 | ||
mount(MyComponent, { | ||
store: createStore(/* ... */), | ||
}); | ||
|
||
mount(MyComponent, { | ||
scopedSlots: { | ||
/* ... */ | ||
}, | ||
}); | ||
``` | ||
|
||
## Limitations | ||
|
||
- This rule cannot detect mount options if they are passed via a variable (eg, `let context = { methods: {} }; mount(Foo, context)` will never error). | ||
- This rule cannot detect mount options passed via object spread or if the mount option keys are not identifiers (eg, `mount(Foo, { ...context }))` and `mount(Foo, { ['methods']: {} })` will never error). | ||
|
||
## When Not To Use It | ||
|
||
- You don't plan to update to Vue 3/VTU 2 | ||
|
||
## Further Reading | ||
|
||
- [VTU 1 mount options](https://vue-test-utils.vuejs.org/api/options.html#context) | ||
- [VTU 2 mount options](https://test-utils.vuejs.org/api/#mount) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
const semver = require('semver'); | ||
|
||
let cachedVtuVersion; | ||
|
||
function detectVtuVersion() { | ||
if (cachedVtuVersion) { | ||
return cachedVtuVersion; | ||
} | ||
|
||
try { | ||
// eslint-disable-next-line node/no-missing-require | ||
const vtuPackageJson = require('@vue/test-utils/package.json'); | ||
if (vtuPackageJson.version) { | ||
return (cachedVtuVersion = vtuPackageJson.version); | ||
} | ||
} catch { | ||
/* intentionally empty */ | ||
} | ||
|
||
throw new Error( | ||
'Unable to detect installed VTU version. Please ensure @vue/test-utils is installed, or set the version explicitly.' | ||
); | ||
} | ||
|
||
/** | ||
* | ||
* @param {string} vtuVersion VTU version to check against | ||
* @param {string} targetVersion version to check | ||
* @returns {boolean} if vtuVersion is greater than or equal to target version | ||
*/ | ||
function isVtuVersionAtLeast(vtuVersion, targetVersion) { | ||
return semver.gte(vtuVersion, targetVersion); | ||
} | ||
|
||
module.exports = isVtuVersionAtLeast; | ||
module.exports.detectVtuVersion = detectVtuVersion; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports.VTU_PLUGIN_SETTINGS_KEY = 'vtu'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
const { get } = require('lodash'); | ||
const path = require('path'); | ||
const isVtuVersionAtLeast = require('./checkVtuVersion'); | ||
const { VTU_PLUGIN_SETTINGS_KEY } = require('./constants'); | ||
const { isVtuImport } = require('./utils'); | ||
const { detectVtuVersion } = isVtuVersionAtLeast; | ||
|
||
/** | ||
* @type {import('eslint').Rule.RuleModule} | ||
*/ | ||
module.exports = { | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'disallow deprecated mount options', | ||
url: path.join(__dirname, '../../docs/rules/no-deprecated-mount-options.md'), | ||
}, | ||
fixable: 'code', | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
ignoreMountOptions: { | ||
description: 'List of mount option property names to ignore', | ||
type: 'array', | ||
items: { | ||
type: 'string', | ||
}, | ||
}, | ||
}, | ||
}, | ||
], // Add a schema if the rule has options | ||
messages: { | ||
deprecatedMountOption: | ||
'The mount option `{{ mountOption }}` is deprecated and will be removed in VTU 2.{{ replacementOption }}', | ||
unknownMountOption: | ||
'The mount option `{{ mountOption }}` is relying on component option merging and will have no effect in VTU 2.', | ||
syncIsRemoved: 'The mount option `sync` was removed in VTU 1.0.0-beta.30 and has no effect.', | ||
}, | ||
}, | ||
|
||
create(context) { | ||
const allowedMountOptions = (context.options[0] && context.options[0].ignoreMountOptions) || []; | ||
const vtuVersion = get(context.settings, [VTU_PLUGIN_SETTINGS_KEY, 'version']) || detectVtuVersion(); | ||
|
||
const sourceCode = context.getSourceCode(); | ||
|
||
const isComma = token => { | ||
return token.type === 'Punctuator' && token.value === ','; | ||
}; | ||
|
||
const getPropertyName = property => | ||
property.key.type === 'Identifier' ? property.key.name : property.key.value; | ||
|
||
function deleteProperty(/** @type {import('eslint').Rule.RuleFixer} */ fixer, property) { | ||
const afterProperty = sourceCode.getTokenAfter(property); | ||
const hasComma = isComma(afterProperty); | ||
|
||
return fixer.removeRange([property.range[0], hasComma ? afterProperty.range[1] : property.range[1]]); | ||
} | ||
|
||
const isVtu2 = isVtuVersionAtLeast(vtuVersion, '2.0.0'); | ||
|
||
const removedOptions = { | ||
// deprecated or replaceable in vtu 1/vue 2 | ||
attachToDocument: { | ||
replacementOption: 'attachTo', | ||
fixer: (/** @type {import('eslint').Rule.RuleFixer} */ fixer, property) => [ | ||
fixer.replaceText(property.key, 'attachTo'), | ||
fixer.replaceText(property.value, 'document.body'), | ||
], | ||
}, | ||
parentComponent: null, | ||
filters: null, | ||
|
||
// removed or moved in vtu 2 | ||
context: null, | ||
listeners: { | ||
replacementOption: 'props', | ||
}, | ||
stubs: { | ||
replacementOption: 'global.stubs', | ||
}, | ||
mocks: { | ||
replacementOption: 'global.mocks', | ||
}, | ||
propsData: { | ||
replacementOption: 'props', | ||
}, | ||
provide: { | ||
replacementOption: 'global.provide', | ||
}, | ||
localVue: { | ||
replacementOption: 'global', | ||
}, | ||
scopedSlots: { | ||
replacementOption: 'slots', | ||
}, | ||
|
||
// not explicitly removed but has trivial replacement | ||
components: { | ||
replacementOption: 'global.components', | ||
}, | ||
directives: { | ||
replacementOption: 'global.directives', | ||
}, | ||
mixins: { | ||
replacementOption: 'global.mixins', | ||
}, | ||
store: { | ||
replacementOption: 'global.plugins', | ||
}, | ||
router: { | ||
replacementOption: 'global.plugins', | ||
}, | ||
}; | ||
|
||
const knownValidMountOptions = isVtu2 | ||
? new Set(['attachTo', 'attrs', 'data', 'props', 'slots', 'global', 'shallow']) | ||
: new Set([ | ||
'context', | ||
'data', | ||
'slots', | ||
'scopedSlots', | ||
'stubs', | ||
'mocks', | ||
'localVue', | ||
'attachTo', | ||
'attrs', | ||
'propsData', | ||
'provide', | ||
'listeners', | ||
|
||
// these properties technically rely on configuration merging | ||
// with the underlying component but are common practice and | ||
// have an autofixable replacement in VTU 2 | ||
'components', | ||
'directives', | ||
'mixins', | ||
'store', | ||
'router', | ||
]); | ||
// add user-whitelisted options | ||
allowedMountOptions.forEach(opt => knownValidMountOptions.add(opt)); | ||
|
||
const mountFunctionNames = new Set(['mount', 'shallowMount']); | ||
|
||
return { | ||
CallExpression(node) { | ||
if (node.callee.type !== 'Identifier' || !mountFunctionNames.has(node.callee.name)) { | ||
return; | ||
} | ||
if (!isVtuImport(node.callee, context.getScope())) { | ||
return; | ||
} | ||
|
||
const mountOptionsNode = node.arguments[1]; | ||
if (!mountOptionsNode || mountOptionsNode.type !== 'ObjectExpression') { | ||
// second argument is not object literal | ||
return; | ||
} | ||
|
||
// filter out object spreads | ||
/** @type {import('estree').Property[]} */ | ||
const properties = mountOptionsNode.properties.filter(({ type }) => type === 'Property'); | ||
|
||
properties.forEach(property => { | ||
if (property.key.type !== 'Identifier' && property.key.type !== 'Literal') { | ||
return; | ||
} | ||
const keyName = getPropertyName(property); | ||
if (keyName === 'sync' && isVtuVersionAtLeast(vtuVersion, '1.0.0-beta.30')) { | ||
context.report({ | ||
messageId: 'syncIsRemoved', | ||
node: property, | ||
fix(fixer) { | ||
return deleteProperty(fixer, property); | ||
}, | ||
}); | ||
} else if (!knownValidMountOptions.has(keyName)) { | ||
context.report({ | ||
messageId: !(keyName in removedOptions) ? 'unknownMountOption' : 'deprecatedMountOption', | ||
node: property, | ||
fix: | ||
removedOptions[keyName] && removedOptions[keyName].fixer | ||
? fixer => removedOptions[keyName].fixer(fixer, property, mountOptionsNode) | ||
: undefined, | ||
data: { | ||
mountOption: keyName, | ||
replacementOption: removedOptions[keyName] | ||
? ` Use '${removedOptions[keyName].replacementOption}' instead.` | ||
: '', | ||
}, | ||
}); | ||
} | ||
}); | ||
}, | ||
}; | ||
}, | ||
}; |
Oops, something went wrong.