├── src ├── rules │ ├── constants.js │ ├── checkVtuVersion.js │ ├── missing-await.js │ ├── no-deprecated-wrapper-functions.js │ ├── utils.js │ ├── no-deprecated-selectors.js │ └── no-deprecated-mount-options.js └── index.js ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── .prettierrc.js ├── .eslintrc.js ├── LICENSE ├── package.json ├── docs └── rules │ ├── index.md │ ├── no-deprecated-wrapper-functions.md │ ├── no-deprecated-mount-options.md │ ├── missing-await.md │ └── no-deprecated-selectors.md ├── .gitignore ├── tests └── src │ └── rules │ ├── no-deprecated-wrapper-functions.test.js │ ├── missing-await.test.js │ ├── no-deprecated-mount-options.test.js │ └── no-deprecated-selectors.test.js └── README.md /src/rules/constants.js: -------------------------------------------------------------------------------- 1 | module.exports.VTU_PLUGIN_SETTINGS_KEY = 'vtu'; 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "orta.vscode-jest"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[javascript]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "[json]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | } 9 | } -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 4, 3 | printWidth: 120, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | trailingComma: 'es5', 9 | bracketSpacing: true, 10 | arrowParens: 'avoid', 11 | }; 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | extends: ['eslint:recommended', 'plugin:eslint-plugin/recommended', 'plugin:node/recommended'], 6 | env: { 7 | node: true, 8 | }, 9 | overrides: [ 10 | { 11 | files: ['tests/**/*.js'], 12 | env: { jest: true }, 13 | }, 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "install", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "problemMatcher": [], 12 | "label": "npm: install", 13 | "detail": "install dependencies from package" 14 | }, 15 | { 16 | "type": "npm", 17 | "script": "test", 18 | "group": { 19 | "kind": "test", 20 | "isDefault": true 21 | }, 22 | "problemMatcher": [], 23 | "label": "npm: test", 24 | "detail": "jest" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Productiv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/rules/checkVtuVersion.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | 3 | let cachedVtuVersion; 4 | 5 | function detectVtuVersion() { 6 | if (cachedVtuVersion) { 7 | return cachedVtuVersion; 8 | } 9 | 10 | try { 11 | // eslint-disable-next-line node/no-missing-require 12 | const vtuPackageJson = require('@vue/test-utils/package.json'); 13 | if (vtuPackageJson.version) { 14 | return (cachedVtuVersion = vtuPackageJson.version); 15 | } 16 | } catch { 17 | /* intentionally empty */ 18 | } 19 | 20 | throw new Error( 21 | 'Unable to detect installed VTU version. Please ensure @vue/test-utils is installed, or set the version explicitly.' 22 | ); 23 | } 24 | 25 | /** 26 | * 27 | * @param {string} vtuVersion VTU version to check against 28 | * @param {string} targetVersion version to check 29 | * @returns {boolean} if vtuVersion is greater than or equal to target version 30 | */ 31 | function isVtuVersionAtLeast(vtuVersion, targetVersion) { 32 | return semver.gte(vtuVersion, targetVersion); 33 | } 34 | 35 | module.exports = isVtuVersionAtLeast; 36 | module.exports.detectVtuVersion = detectVtuVersion; 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const missingAwait = require('./rules/missing-await'); 2 | const noDeprecatedMountOptions = require('./rules/no-deprecated-mount-options'); 3 | const noDeprecatedSelectors = require('./rules/no-deprecated-selectors'); 4 | const noDeprecatedWrappers = require('./rules/no-deprecated-wrapper-functions'); 5 | 6 | //------------------------------------------------------------------------------ 7 | // Plugin Definition 8 | //------------------------------------------------------------------------------ 9 | 10 | module.exports.rules = { 11 | 'missing-await': missingAwait, 12 | 'no-deprecated-mount-options': noDeprecatedMountOptions, 13 | 'no-deprecated-wrapper-functions': noDeprecatedWrappers, 14 | 'no-deprecated-selectors': noDeprecatedSelectors, 15 | }; 16 | 17 | module.exports.configs = { 18 | recommended: { 19 | plugins: ['vue-test-utils'], 20 | rules: { 21 | 'vue-test-utils/missing-await': 'error', 22 | 'vue-test-utils/no-deprecated-mount-options': 'error', 23 | 'vue-test-utils/no-deprecated-selectors': 'error', 24 | 'vue-test-utils/no-deprecated-wrapper-functions': 'error', 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-vue-test-utils", 3 | "version": "1.0.1", 4 | "description": "Linting for Vue Test Utils", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Productiv", 8 | "url": "https://github.com/BeProductiv" 9 | }, 10 | "repository": { 11 | "url": "https://github.com/BeProductiv/eslint-plugin-vue-test-utils.git" 12 | }, 13 | "keywords": [ 14 | "eslint", 15 | "eslintplugin", 16 | "eslint-plugin", 17 | "@vue/test-utils", 18 | "vue", 19 | "test" 20 | ], 21 | "main": "src/index.js", 22 | "files": [ 23 | "docs", 24 | "src", 25 | "README.md" 26 | ], 27 | "scripts": { 28 | "lint": "eslint .", 29 | "lint:fix": "eslint --fix .", 30 | "test": "jest", 31 | "prettier": "prettier -cw ." 32 | }, 33 | "dependencies": { 34 | "semver": "^6.0.0", 35 | "lodash": "^4.17.21" 36 | }, 37 | "devDependencies": { 38 | "eslint": "^6.2.1", 39 | "eslint-plugin-eslint-plugin": "^4.0.1", 40 | "eslint-plugin-node": "^11.1.0", 41 | "jest": "^28.1.1", 42 | "prettier": "^2.6.2" 43 | }, 44 | "engines": { 45 | "node": "14.x || >= 16" 46 | }, 47 | "peerDependencies": { 48 | "eslint": ">=6" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/rules/index.md: -------------------------------------------------------------------------------- 1 | This plugin provides the following rules: 2 | 3 | # [vue-test-utils/missing-await](./missing-await.md) 4 | 5 | Checks that Wrapper functions which trigger component updates are awaited. 6 | 7 | This rule helps test writers make sure they have waited for any component updates to complete before trying to assert on them, preventing "but nothing happened!" syndrome. 8 | 9 | # [vue-test-utils/no-deprecated-mount-options](./no-deprecated-mount-options.md) 10 | 11 | Checks that mount and shallowMount options are valid for the current version of VTU. 12 | 13 | This rule reports when mount options that are deprecated or unsupported in the current version of VTU are used. 14 | 15 | # [vue-test-utils/no-deprecated-selectors](./no-deprecated-selectors.md) 16 | 17 | Checks that Wrapper methods are called with appropriate selectors. 18 | 19 | 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. 20 | 21 | Additionally, 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. 22 | 23 | # [vue-test-utils/no-deprecated-wrapper-functions](./no-deprecated-wrapper-functions.md) 24 | 25 | Checks that no Wrapper functions which are deprecated are used. 26 | This rule helps with upgrading VTU from v1 to v2 by warning of function calls which are deprecated and have been removed in v2. 27 | This rule reports `Wrapper` instance method calls which are deprecated in `@vue/test-utils` v2. 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /docs/rules/no-deprecated-wrapper-functions.md: -------------------------------------------------------------------------------- 1 | # Checks that no Wrapper functions which are deprecated are used. (vue-test-utils/no-deprecated-wrapper-functions) 2 | 3 | This rule helps with upgrading VTU from v1 to v2 by warning of function calls which are deprecated and have been removed in v2. 4 | 5 | ## Rule Details 6 | 7 | This rule reports `Wrapper` instance method calls which are deprecated in `@vue/test-utils` v2. 8 | 9 | ### Options 10 | 11 | This rule has an object option: 12 | 13 | - `wrapperNames` can be set to an array of variables names that are checked for deprecated function calls. 14 | 15 | Examples of **incorrect** code for this rule: 16 | 17 | ```js 18 | /* eslint vue-test-utils/no-deprecated-wrapper-functions: "error" */ 19 | const wrapper = mount(MyComponent); 20 | 21 | expect(wrapper.is('div')).toBe(false); 22 | expect(wrapper.contains('div')).toBe(false); 23 | ``` 24 | 25 | Examples of **correct** code for this rule: 26 | 27 | ```js 28 | /* eslint vue-test-utils/no-deprecated-wrapper-functions: "error" */ 29 | const wrapper = mount(MyComponent); 30 | 31 | expect(wrapper.element.tagName).toEqual('div'); 32 | expect(wrapper.find('div')).toBeTruthy(); 33 | ``` 34 | 35 | Examples of **incorrect** code with the `{ "wrapperName": ["component"] }` option: 36 | 37 | ```js 38 | /* eslint vue-test-utils/no-deprecated-wrapper-functions: ["error", { "wrapperName": ["component"] }] */ 39 | const component = mount(MyComponent); 40 | 41 | expect(component.is('div')).toBe(false); 42 | expect(component.contains('div')).toBe(false); 43 | ``` 44 | 45 | Examples of **correct** code with the `{ "wrapperName": ["component"] }` option: 46 | 47 | ```js 48 | /* eslint vue-test-utils/no-deprecated-wrapper-functions: ["error", { "wrapperName": ["component"] }] */ 49 | const component = mount(MyComponent); 50 | 51 | expect(component.element.tagName).toEqual('div'); 52 | expect(component.find('div')).toBeTruthy(); 53 | 54 | const wrapper = mount(MyComponent); 55 | 56 | // these are not reported because `wrapper` is not in the list of `wrapperName`s 57 | expect(wrapper.contains('div')).toBe(false); 58 | ``` 59 | 60 | ## Limitations 61 | 62 | - This rule cannot detect wrappers if they are not stored into a local variable (eg, `mount(Foo).contains('div')` will never error) 63 | 64 | ## When Not To Use It 65 | 66 | - When you are using VTU 2 already 67 | 68 | ## Further Reading 69 | 70 | - [VTU 2 Wrapper compatibility list](https://github.com/vuejs/test-utils#wrapper-api-mount) 71 | -------------------------------------------------------------------------------- /tests/src/rules/no-deprecated-wrapper-functions.test.js: -------------------------------------------------------------------------------- 1 | const rule = require('../../../src/rules/no-deprecated-wrapper-functions'), 2 | RuleTester = require('eslint').RuleTester; 3 | 4 | const removedFunctionNames = [ 5 | 'contains', 6 | 'emittedByOrder', 7 | 'setSelected', 8 | 'setChecked', 9 | 'is', 10 | 'isEmpty', 11 | 'isVueInstance', 12 | 'name', 13 | 'setMethods', 14 | ]; 15 | 16 | const alternativeSuggestions = { 17 | contains: 'exists()', 18 | is: 'classes(), attributes(), or element.tagName', 19 | isEmpty: 'exists(), isVisible(), or a custom matcher from jest-dom', 20 | name: 'vm.$options.name', 21 | emittedByOrder: 'emitted()', 22 | }; 23 | 24 | const ruleTester = new RuleTester(); 25 | ruleTester.run('no-deprecated-wrapper-functions', rule, { 26 | valid: [ 27 | { code: 'wrapper.html()' }, 28 | { code: 'wrapper.contains(MyComponent)', options: [{ wrapperNames: ['foo'] }] }, // normally illegal but passes because of options 29 | ], 30 | 31 | invalid: [ 32 | ...removedFunctionNames.map(functionName => ({ 33 | code: `wrapper.${functionName}('div')`, 34 | errors: [ 35 | { 36 | messageId: 'deprecatedFunction', 37 | data: { 38 | identifier: functionName, 39 | alternativeSuggestion: alternativeSuggestions[functionName] 40 | ? ` Consider using ${alternativeSuggestions[functionName]} instead.` 41 | : '', 42 | }, 43 | }, 44 | ], 45 | })), 46 | { 47 | // chained functions with at() 48 | code: "wrapper.findAllComponents(MyComponent).at(2).contains('div')", 49 | errors: [ 50 | { 51 | messageId: 'deprecatedFunction', 52 | data: { identifier: 'contains', alternativeSuggestion: ' Consider using exists() instead.' }, 53 | }, 54 | ], 55 | }, 56 | { 57 | // chained functions 58 | code: "wrapper.get(MyComponent).contains('div')", 59 | errors: [ 60 | { 61 | messageId: 'deprecatedFunction', 62 | data: { identifier: 'contains', alternativeSuggestion: ' Consider using exists() instead.' }, 63 | }, 64 | ], 65 | }, 66 | { 67 | // wrapperNames option 68 | code: 'foo.contains(MyComponent)', 69 | options: [{ wrapperNames: ['foo'] }], 70 | errors: [ 71 | { 72 | messageId: 'deprecatedFunction', 73 | data: { identifier: 'contains', alternativeSuggestion: ' Consider using exists() instead.' }, 74 | }, 75 | ], 76 | }, 77 | ], 78 | }); 79 | -------------------------------------------------------------------------------- /tests/src/rules/missing-await.test.js: -------------------------------------------------------------------------------- 1 | const rule = require('../../../src/rules/missing-await'), 2 | RuleTester = require('eslint').RuleTester; 3 | 4 | const functionsToCheck = ['setChecked', 'setData', 'setMethods', 'setProps', 'setSelected', 'setValue', 'trigger']; 5 | 6 | const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); 7 | ruleTester.run('missing-await', rule, { 8 | valid: [ 9 | ...functionsToCheck.map(fn => ({ 10 | code: `async () => await wrapper.${fn}()`, 11 | })), 12 | { code: 'async () => await wrapper.vm.$emit("click")' }, 13 | { code: 'async () => await wrapper.getComponent(MyComponent).vm.$emit("click")' }, 14 | { code: 'async () => await wrapper.findComponent(MyComponent).vm.$emit("click")' }, 15 | { code: 'async () => await wrapper.findAllComponents(MyComponent).at(0).vm.$emit("click")' }, 16 | { code: 'wrapper.setChecked(MyComponent)', options: [{ wrapperNames: ['foo'] }] }, // normally illegal but passes because of options 17 | ], 18 | 19 | invalid: [ 20 | ...functionsToCheck.map(fn => ({ 21 | code: `() => wrapper.${fn}()`, 22 | errors: [{ messageId: 'missingAwait', data: { identifier: fn } }], 23 | output: `async () => await wrapper.${fn}()`, 24 | })), 25 | ...functionsToCheck.map(fn => ({ 26 | code: `async () => wrapper.${fn}()`, 27 | errors: [{ messageId: 'missingAwait', data: { identifier: fn } }], 28 | output: `async () => await wrapper.${fn}()`, 29 | })), 30 | { 31 | code: '() => wrapper.vm.$emit("click")', 32 | errors: [{ messageId: 'missingAwait', data: { identifier: 'vm.$emit' } }], 33 | output: `async () => await wrapper.vm.$emit("click")`, 34 | }, 35 | { 36 | code: '() => wrapper.getComponent(MyComponent).vm.$emit("click")', 37 | errors: [{ messageId: 'missingAwait', data: { identifier: 'vm.$emit' } }], 38 | output: `async () => await wrapper.getComponent(MyComponent).vm.$emit("click")`, 39 | }, 40 | { 41 | code: '() => wrapper.findComponent(MyComponent).vm.$emit("click")', 42 | errors: [{ messageId: 'missingAwait', data: { identifier: 'vm.$emit' } }], 43 | output: `async () => await wrapper.findComponent(MyComponent).vm.$emit("click")`, 44 | }, 45 | { 46 | code: '() => wrapper.findAllComponents(MyComponent).at(0).vm.$emit("click")', 47 | errors: [{ messageId: 'missingAwait', data: { identifier: 'vm.$emit' } }], 48 | output: `async () => await wrapper.findAllComponents(MyComponent).at(0).vm.$emit("click")`, 49 | }, 50 | { 51 | // wrapperNames option 52 | code: '() => foo.trigger(MyComponent)', 53 | options: [{ wrapperNames: ['foo'] }], 54 | errors: [{ messageId: 'missingAwait' }], 55 | output: `async () => await foo.trigger(MyComponent)`, 56 | }, 57 | ], 58 | }); 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-vue-test-utils 2 | 3 | [![npm version](https://badge.fury.io/js/eslint-plugin-vue-test-utils.svg)](https://badge.fury.io/js/eslint-plugin-vue-test-utils) 4 | 5 | Linting for [@vue/test-utils](https://github.com/vuejs/test-utils). 6 | 7 | ## Installation 8 | 9 | ``` 10 | npm install --save-dev eslint eslint-plugin-vue-test-utils 11 | ``` 12 | 13 | ## Usage 14 | 15 | Add `vue-test-utils` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: 16 | 17 | ```json 18 | { 19 | "plugins": ["vue-test-utils"] 20 | } 21 | ``` 22 | 23 | Then configure the rules you want to use under the rules section. 24 | 25 | ```json 26 | { 27 | "rules": { 28 | "vue-test-utils/no-deprecated-wrapper-functions": "error" 29 | } 30 | } 31 | ``` 32 | 33 | Alternatively, extend the recommended configuration: 34 | 35 | ```json 36 | { 37 | "extends": [ 38 | "eslint-recommended", 39 | // ... 40 | "plugin:vue-test-utils/recommended" 41 | ] 42 | } 43 | ``` 44 | 45 | The recommended configuration will enable all rules as errors. Specific rules can be overriden 46 | as described above. 47 | 48 | ### Setting the VTU version 49 | 50 | Some rules have different behavior depending on the version of VTU that is being used. If the plugin and VTU are not installed in the same directory (possible in some monorepo configurations), VTU's version will not be able to be auto-detected and you will get an error. In this case, you can set it manually in your `.eslintrc`: 51 | 52 | ```json 53 | { 54 | "settings": { 55 | "vtu": { 56 | "version": "1.3.0" 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | If your `.eslintrc` file is a Javascript file instead of JSON, you may be able to use `require('@vue/test-utils/package.json').version` to pick up the version directly from the installed package. 63 | 64 | ## Supported Rules 65 | 66 | [See rules](./docs/rules/index.md) for a full list of rules enabled by this plugin. 67 | 68 | ## Adding new rules 69 | 70 | Create a new rule file, test file, and docs file in `./src/rules`, `./test/src/rules`, and `./docs/rules` respectively. Import the rule file in `./src/index.js` and add it to the list of rules exports and to the recommended config rules. Use the existing code as guidance. For more details on how to write a rule, here are some useful links to get you started: 71 | 72 | - View the syntax tree of your code: [ASTExplorer.net](https://astexplorer.net/) 73 | - [ESLint developer guide for rules](https://eslint.org/docs/developer-guide/working-with-rules) 74 | - Autofixing your rule: [Applying fixes guide](https://eslint.org/docs/developer-guide/working-with-rules#applying-fixes) 75 | - Testing your rule: [`RuleTester` documentation](https://eslint.org/docs/developer-guide/nodejs-api#ruletester) 76 | 77 | Note that when exporting the rule, you use the unprefixed ID, but when adding the rule to the configuration, you need to use the fully-qualified name of the rule (in the format `vue-test-utils/{id}`). 78 | 79 | ## License 80 | 81 | [MIT](./LICENSE) 82 | -------------------------------------------------------------------------------- /docs/rules/no-deprecated-mount-options.md: -------------------------------------------------------------------------------- 1 | # Checks that mount and shallowMount options are valid for the current version of VTU. (vue-test-utils/no-deprecated-mount-options) 2 | 3 | The `--fix` option on the command line can automatically fix some of the problems reported by this rule. 4 | 5 | ## Rule Details 6 | 7 | This rule reports when mount options that are deprecated or unsupported in the current version of VTU are used. 8 | 9 | ### Options 10 | 11 | This rule has an object option: 12 | 13 | - `ignoreMountOptions` can be set to an array of property names that are ignored when checking for deprecated mount options. 14 | - 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`. 15 | 16 | Examples of **incorrect** code for this rule: 17 | 18 | ```js 19 | /* eslint vue-test-utils/no-deprecated-mount-options: "error" */ 20 | import { mount } from '@vue/test-utils'; 21 | 22 | // VTU 1 23 | mount(MyComponent, { 24 | attachToDocument: true, 25 | }); 26 | 27 | mount(MyComponent, { 28 | computed: { /* ... */ } 29 | methods: { /* ... */ }, 30 | }); 31 | 32 | // VTU 2 33 | mount(MyComponent, { 34 | propsData: { /* ... */ } 35 | }) 36 | 37 | mount(MyComponent, { 38 | stubs: [/* ... */] 39 | }) 40 | ``` 41 | 42 | Examples of **correct** code for this rule: 43 | 44 | ```js 45 | /* eslint vue-test-utils/no-deprecated-mount-options: "error" */ 46 | import { mount } from '@vue/test-utils'; 47 | 48 | // VTU 1 49 | mount(MyComponent, { 50 | attachTo: document.body, 51 | }); 52 | 53 | mount(MyComponent, { 54 | mixins: [ 55 | /* ... */ 56 | ], 57 | }); 58 | 59 | // VTU 2 60 | mount(MyComponent, { 61 | props: { 62 | /* ... */ 63 | }, 64 | }); 65 | 66 | mount(MyComponent, { 67 | global: { 68 | stubs: [ 69 | /* ... */ 70 | ], 71 | }, 72 | }); 73 | ``` 74 | 75 | Examples of **correct** code with the `{ "ignoreMountOptions": ["store", "scopedSlots"] }` option: 76 | 77 | ```js 78 | /* eslint vue-test-utils/no-deprecated-mount-options: ["error", { "ignoreMountOptions": ["store", "scopedSlots"] }] */ 79 | import { mount } from '@vue/test-utils'; 80 | 81 | // VTU 2 82 | mount(MyComponent, { 83 | store: createStore(/* ... */), 84 | }); 85 | 86 | mount(MyComponent, { 87 | scopedSlots: { 88 | /* ... */ 89 | }, 90 | }); 91 | ``` 92 | 93 | ## Limitations 94 | 95 | - This rule cannot detect mount options if they are passed via a variable (eg, `let context = { methods: {} }; mount(Foo, context)` will never error). 96 | - 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). 97 | 98 | ## When Not To Use It 99 | 100 | - You don't plan to update to Vue 3/VTU 2 101 | 102 | ## Further Reading 103 | 104 | - [VTU 1 mount options](https://vue-test-utils.vuejs.org/api/options.html#context) 105 | - [VTU 2 mount options](https://test-utils.vuejs.org/api/#mount) 106 | -------------------------------------------------------------------------------- /docs/rules/missing-await.md: -------------------------------------------------------------------------------- 1 | # Checks that Wrapper functions which trigger component updates are awaited. (vue-test-utils/missing-await) 2 | 3 | The `--fix` option on the command line can automatically fix the problems reported by this rule. 4 | 5 | This rule helps test writers make sure they have waited for any component updates to complete before trying to assert on them, preventing "but nothing happened!" syndrome. 6 | 7 | ## Rule Details 8 | 9 | This rule reports wrapper function calls which return Promises and probably need to be awaited to observe their effect on the component wrapped by the wrapper. 10 | 11 | ### Options 12 | 13 | This rule has an object option: 14 | 15 | - `wrapperNames` can be set to an array of variables names that are checked for deprecated function calls. 16 | 17 | Examples of **incorrect** code for this rule: 18 | 19 | ```js 20 | /* eslint vue-test-utils/missing-await: "error" */ 21 | it('does the thing', () => { 22 | const wrapper = mount(MyComponent); 23 | 24 | wrapper.get('button').trigger('click'); 25 | wrapper.getComponent(Foo).vm.$emit('click'); 26 | 27 | expect(wrapper.findAll('div').at(0)).toBeTruthy(); 28 | }); 29 | ``` 30 | 31 | Examples of **correct** code for this rule: 32 | 33 | ```js 34 | /* eslint vue-test-utils/missing-await: "error" */ 35 | it('does the thing', async () => { 36 | const wrapper = mount(MyComponent); 37 | 38 | await wrapper.get('button').trigger('click'); 39 | await wrapper.getComponent(MyComponent).vm.$emit('click'); 40 | 41 | expect(wrapper.findAll('div').at(0)).toBeTruthy(); 42 | }); 43 | ``` 44 | 45 | Examples of **incorrect** code with the `{ "wrapperName": ["component"] }` option: 46 | 47 | ```js 48 | /* eslint vue-test-utils/missing-await: ["error", { "wrapperName": ["component"] }] */ 49 | it('does the thing', () => { 50 | const component = mount(MyComponent); 51 | 52 | component.get('button').trigger('click'); 53 | component.getComponent(Foo).vm.$emit('click'); 54 | 55 | expect(component.findAll('div').at(0)).toBeTruthy(); 56 | }); 57 | ``` 58 | 59 | Examples of **correct** code with the `{ "wrapperName": ["component"] }` option: 60 | 61 | ```js 62 | /* eslint vue-test-utils/missing-await: ["error", { "wrapperName": ["component"] }] */ 63 | it('does the thing', async () => { 64 | const component = mount(MyComponent); 65 | 66 | await component.get('button').trigger('click'); 67 | await component.getComponent(MyComponent).vm.$emit('click'); 68 | 69 | expect(component.findAll('div').at(0)).toBeTruthy(); 70 | }); 71 | 72 | it("doesn't the thing", () => { 73 | const wrapper = mount(MyComponent); 74 | 75 | // not reported because `wrapper` is not in the list of `wrapperName`s 76 | wrapper.getComponent(MyComponent).vm.$emit('click'); 77 | }); 78 | ``` 79 | 80 | ## Limitations 81 | 82 | - This rule cannot detect wrappers if they are not stored into a local variable (eg, `mount(Foo).trigger('click')` will never error) 83 | 84 | ## When Not To Use It 85 | 86 | - You are manually flushing component updates with `wrapper.$vm.nextTick()` or `flushPromises()` 87 | - You are using a VTU version from before these methods were made async 88 | 89 | ## Further Reading 90 | 91 | - [VTU 1 docs](https://vue-test-utils.vuejs.org/api/wrapper/#trigger) 92 | -------------------------------------------------------------------------------- /docs/rules/no-deprecated-selectors.md: -------------------------------------------------------------------------------- 1 | # Checks that Wrapper methods are called with appropriate selectors. (vue-test-utils/no-deprecated-selectors) 2 | 3 | The `--fix` option on the command line can automatically fix some of the problems reported by this rule. 4 | 5 | ## Rule Details 6 | 7 | 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. 8 | 9 | Additionally, 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. 10 | 11 | ### Options 12 | 13 | This rule has an object option: 14 | 15 | - `wrapperNames` can be set to an array of variable names that are checked for deprecated function calls. 16 | 17 | Examples of **incorrect** code for this rule: 18 | 19 | ```js 20 | /* eslint vue-test-utils/no-deprecated-selectors: "error" */ 21 | import MyComponent from './MyComponent.vue'; 22 | 23 | const wrapper = mount(MyComponent); 24 | 25 | wrapper.get('div').vm.$emit('click'); 26 | wrapper.get(MyComponent).setProps(/* ... */); 27 | expect(wrapper.findAll(FooComponent)).at(0)).toBeTruthy(); 28 | ``` 29 | 30 | Examples of **correct** code for this rule: 31 | 32 | ```js 33 | /* eslint vue-test-utils/no-deprecated-selectors: "error" */ 34 | import MyComponent from './MyComponent.vue'; 35 | 36 | const wrapper = mount(MyComponent); 37 | 38 | wrapper.getComponent(DivComponent).vm.$emit('click'); 39 | wrapper.getComponent(MyComponent).setProps(/* ... */); 40 | expect(wrapper.findAllComponents(FooComponent).at(0)).toBeTruthy(); 41 | ``` 42 | 43 | Examples of **incorrect** code with the `{ "wrapperName": ["component"] }` option: 44 | 45 | ```js 46 | /* eslint vue-test-utils/no-deprecated-selectors: ["error", { "wrapperName": ["component"] }] */ 47 | import MyComponent from './MyComponent.vue'; 48 | 49 | const component = mount(MyComponent); 50 | 51 | component.get('div').vm.$emit('click'); 52 | component.get(MyComponent).setProps(/* ... */); 53 | expect(component.findAll(FooComponent).at(0)).toBeTruthy(); 54 | ``` 55 | 56 | Examples of **correct** code with the `{ "wrapperName": ["component"] }` option: 57 | 58 | ```js 59 | /* eslint vue-test-utils/no-deprecated-selectors: ["error", { "wrapperName": ["component"] }] */ 60 | import MyComponent from './MyComponent.vue'; 61 | 62 | const component = mount(MyComponent); 63 | 64 | component.getComponent(DivComponent).vm.$emit('click'); 65 | component.getComponent(MyComponent).setProps(/* ... */); 66 | 67 | const wrapper = mount(MyComponent); 68 | 69 | // not reported because `wrapper` is not in the list of `wrapperName`s 70 | wrapper.get(MyComponent).vm.$emit('click'); 71 | ``` 72 | 73 | ## Limitations 74 | 75 | - 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) 76 | 77 | ## When Not To Use It 78 | 79 | - Never 80 | 81 | ## Further Reading 82 | 83 | - [VTU 1 docs](https://vue-test-utils.vuejs.org/api/wrapper/#find) 84 | -------------------------------------------------------------------------------- /src/rules/missing-await.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { nodeIsCalledFromWrapper, nodeIsComponentEmit } = require('./utils'); 3 | 4 | const DEFAULT_WRAPPER_VARIABLES = ['wrapper']; 5 | 6 | /** 7 | * @type {import('eslint').Rule.RuleModule} 8 | */ 9 | module.exports = { 10 | meta: { 11 | type: 'problem', 12 | docs: { 13 | description: 'Reports when wrapper methods which update the underlying component are not awaited', 14 | url: path.join(__dirname, '../../docs/rules/missing-await.md'), 15 | }, 16 | fixable: 'code', 17 | schema: [ 18 | { 19 | type: 'object', 20 | properties: { 21 | wrapperNames: { 22 | description: 'List of variable names to which wrappers are typically assigned', 23 | type: 'array', 24 | items: { 25 | type: 'string', 26 | }, 27 | }, 28 | }, 29 | }, 30 | ], // Add a schema if the rule has options 31 | messages: { 32 | missingAwait: 33 | 'wrapper.{{ identifier }}() should be awaited to ensure resulting component updates are visible', 34 | }, 35 | }, 36 | 37 | create(context) { 38 | const wrapperNames = (context.options[0] && context.options[0].wrapperNames) || DEFAULT_WRAPPER_VARIABLES; 39 | 40 | const functionsTriggeringComponentUpdate = new Set([ 41 | 'setChecked', 42 | 'setData', 43 | 'setMethods', 44 | 'setProps', 45 | 'setSelected', 46 | 'setValue', 47 | 'trigger', 48 | ]); 49 | 50 | function getContainingFunction(node) { 51 | while (node && !['ArrowFunctionExpression', 'FunctionExpression'].includes(node.type)) { 52 | node = node.parent; 53 | } 54 | return node; 55 | } 56 | 57 | function callTriggersUpdate(node) { 58 | if (node.callee.property.type !== 'Identifier') { 59 | return false; 60 | } 61 | 62 | const isWrapperUpdateFunction = functionsTriggeringComponentUpdate.has(node.callee.property.name); 63 | const isVmEmit = nodeIsComponentEmit(node); 64 | 65 | return ( 66 | (isWrapperUpdateFunction || isVmEmit) && 67 | nodeIsCalledFromWrapper(isVmEmit ? node.callee.object.object : node.callee.object, wrapperNames) 68 | ); 69 | } 70 | 71 | return { 72 | CallExpression(node) { 73 | if (node.callee.type !== 'MemberExpression') { 74 | return; 75 | } 76 | 77 | if (callTriggersUpdate(node) && node.parent && node.parent.type !== 'AwaitExpression') { 78 | context.report({ 79 | messageId: 'missingAwait', 80 | node: node, 81 | data: { 82 | identifier: node.callee.property.name === '$emit' ? 'vm.$emit' : node.callee.property.name, 83 | }, 84 | fix(fixer) { 85 | const fixes = [fixer.insertTextBefore(node, 'await ')]; 86 | const containingFunction = getContainingFunction(node); 87 | if (containingFunction && !containingFunction.async) { 88 | fixes.push(fixer.insertTextBefore(containingFunction, 'async ')); 89 | } 90 | return fixes; 91 | }, 92 | }); 93 | return; 94 | } 95 | }, 96 | }; 97 | }, 98 | }; 99 | -------------------------------------------------------------------------------- /src/rules/no-deprecated-wrapper-functions.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { nodeIsCalledFromWrapper } = require('./utils'); 3 | 4 | const DEFAULT_WRAPPER_VARIABLES = ['wrapper']; 5 | 6 | /** 7 | * @type {import('eslint').Rule.RuleModule} 8 | */ 9 | module.exports = { 10 | meta: { 11 | type: 'problem', 12 | docs: { 13 | description: 'disallow deprecated Wrapper functions', 14 | url: path.join(__dirname, '../../docs/rules/no-deprecated-wrapper-functions.md'), 15 | }, 16 | fixable: 'code', 17 | schema: [ 18 | { 19 | type: 'object', 20 | properties: { 21 | wrapperNames: { 22 | description: 'List of variable names to which wrappers are typically assigned', 23 | type: 'array', 24 | items: { 25 | type: 'string', 26 | }, 27 | }, 28 | }, 29 | }, 30 | ], // Add a schema if the rule has options 31 | messages: { 32 | deprecatedFunction: 33 | '{{ identifier }} is deprecated and will be removed in VTU 2.{{ alternativeSuggestion }}', 34 | }, 35 | }, 36 | 37 | create(context) { 38 | const wrapperNames = (context.options[0] && context.options[0].wrapperNames) || DEFAULT_WRAPPER_VARIABLES; 39 | 40 | const deprecatedFunctionNames = new Set([ 41 | 'emittedByOrder', 42 | 'contains', 43 | 'is', 44 | 'isEmpty', 45 | 'isVueInstance', 46 | 'name', 47 | 'setMethods', 48 | // these two are kind of weird. they are rolled into setValue() in VTU 2 but I don't think 49 | // setValue worked to replace these in VTU 1 - or at least, I don't know for sure that they do. 50 | 'setSelected', 51 | 'setChecked', 52 | ]); 53 | 54 | const alternativeSuggestions = { 55 | contains: 'exists()', 56 | is: 'classes(), attributes(), or element.tagName', 57 | isEmpty: 'exists(), isVisible(), or a custom matcher from jest-dom', 58 | name: 'vm.$options.name', 59 | emittedByOrder: 'emitted()', 60 | }; 61 | 62 | const autofixableFunctions = { 63 | /** 64 | * 65 | * @param {*} node 66 | * @param {import('eslint').Rule.RuleFixer} fixer 67 | */ 68 | contains: (node, fixer) => { 69 | return [fixer.replaceText(node.callee.property, 'find'), fixer.insertTextAfter(node, '.exists()')]; 70 | }, 71 | }; 72 | 73 | return { 74 | CallExpression(node) { 75 | if (node.callee.type !== 'MemberExpression') { 76 | return; 77 | } 78 | 79 | if ( 80 | node.callee.property.type === 'Identifier' && 81 | deprecatedFunctionNames.has(node.callee.property.name) && 82 | nodeIsCalledFromWrapper(node.callee.object, wrapperNames) 83 | ) { 84 | context.report({ 85 | messageId: 'deprecatedFunction', 86 | node: node.callee.property, 87 | data: { 88 | identifier: node.callee.property.name, 89 | alternativeSuggestion: alternativeSuggestions[node.callee.property.name] 90 | ? ` Consider using ${alternativeSuggestions[node.callee.property.name]} instead.` 91 | : '', 92 | }, 93 | fix: 94 | autofixableFunctions[node.callee.property.name] && 95 | autofixableFunctions[node.callee.property.name].bind(this, node), 96 | }); 97 | return; 98 | } 99 | }, 100 | }; 101 | }, 102 | }; 103 | -------------------------------------------------------------------------------- /tests/src/rules/no-deprecated-mount-options.test.js: -------------------------------------------------------------------------------- 1 | const { flatMap } = require('lodash'); 2 | 3 | const rule = require('../../../src/rules/no-deprecated-mount-options'), 4 | RuleTester = require('eslint').RuleTester; 5 | 6 | function genCodeString(mountFunctionName, mountFunctionOptions) { 7 | return ` 8 | import { ${mountFunctionName} } from '@vue/test-utils'; 9 | ${mountFunctionName}(MyComponent, ${ 10 | typeof mountFunctionOptions === 'object' ? JSON.stringify(mountFunctionOptions) : mountFunctionOptions 11 | }); 12 | `; 13 | } 14 | 15 | function mount(options) { 16 | return genCodeString('mount', options); 17 | } 18 | 19 | function shallowMount(options) { 20 | return genCodeString('shallowMount', options); 21 | } 22 | 23 | function makePassingTestCases(tests) { 24 | return flatMap(tests, ([mountOptions, ignoreMountOptions]) => [ 25 | { code: mount(mountOptions), ...(ignoreMountOptions ? { options: [{ ignoreMountOptions }] } : {}) }, 26 | { 27 | code: shallowMount(mountOptions), 28 | ...(ignoreMountOptions ? { options: [{ ignoreMountOptions }] } : {}), 29 | }, 30 | ]); 31 | } 32 | 33 | function makeFailingTestCases(tests) { 34 | return flatMap(tests, ([mountOptions, errors, { ignoreMountOptions, fixedMountOptions } = {}]) => 35 | [mount, shallowMount].map(fn => ({ 36 | code: fn(mountOptions), 37 | errors: Array.isArray(errors) ? errors : [errors], 38 | ...(ignoreMountOptions ? { options: [{ ignoreMountOptions }] } : {}), 39 | ...(fixedMountOptions ? { output: fn(fixedMountOptions) } : {}), 40 | })) 41 | ); 42 | } 43 | 44 | function makeDeprecatedMountOptionTestCase(mountOptionName, replacement) { 45 | return [ 46 | { [mountOptionName]: {} }, 47 | { 48 | messageId: 'deprecatedMountOption', 49 | data: { 50 | mountOption: mountOptionName, 51 | replacementOption: replacement ? ` Use '${replacement}' instead.` : '', 52 | }, 53 | }, 54 | ]; 55 | } 56 | 57 | const falsePositiveTests = [ 58 | { code: 'mount({ sync: true })' }, 59 | { code: 'app.mount({ sync: true })' }, 60 | { code: 'import { mount } from "@vue/test-utils"; app.mount({ sync: true });' }, 61 | { code: 'import { mount } from "enzyme"; mount({ sync: true });' }, 62 | ]; 63 | 64 | const vtu1InvalidTests = makeFailingTestCases([ 65 | /* mountOptions, error(s), fixedOptions/pluginOptions */ 66 | [{ sync: true }, { messageId: 'syncIsRemoved' }, { fixedMountOptions: {} }], 67 | [ 68 | { attachToDocument: true }, 69 | { 70 | messageId: 'deprecatedMountOption', 71 | data: { mountOption: 'attachToDocument', replacementOption: " Use 'attachTo' instead." }, 72 | }, 73 | { fixedMountOptions: '{attachTo:document.body}' }, 74 | ], 75 | makeDeprecatedMountOptionTestCase('filters'), 76 | [ 77 | { methods: {}, computed: {} }, 78 | ['methods', 'computed'].map(opt => ({ 79 | messageId: 'unknownMountOption', 80 | data: { mountOption: opt }, 81 | })), 82 | ], 83 | ]); 84 | 85 | describe('VTU 1', () => { 86 | const ruleTester = new RuleTester({ 87 | parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, 88 | settings: { vtu: { version: '1.3.0' } }, 89 | }); 90 | ruleTester.run('no-deprecated-mount-options', rule, { 91 | valid: [ 92 | ...falsePositiveTests, 93 | ...makePassingTestCases([ 94 | [ 95 | { 96 | data() {}, 97 | slots: {}, 98 | scopedSlots: {}, 99 | stubs: [], 100 | mocks: {}, 101 | localVue: {}, 102 | attachTo: null, 103 | attrs: {}, 104 | propsData: {}, 105 | provide: {}, 106 | listeners: {}, 107 | components: {}, 108 | directives: {}, 109 | mixins: {}, 110 | store: {}, 111 | router: {}, 112 | }, 113 | ], 114 | [{ filters: {} }, ['filters']], 115 | ]), 116 | ], 117 | 118 | invalid: vtu1InvalidTests, 119 | }); 120 | }); 121 | 122 | describe('VTU 2', () => { 123 | const ruleTester = new RuleTester({ 124 | parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, 125 | settings: { vtu: { version: '2.0.0' } }, 126 | }); 127 | ruleTester.run('no-deprecated-mount-options', rule, { 128 | valid: [ 129 | ...falsePositiveTests, 130 | ...makePassingTestCases([ 131 | [{ attachTo: null, attrs: {}, data() {}, props: {}, slots: {}, global: {}, shallow: true }], 132 | [ 133 | { 134 | attachTo: null, 135 | attrs: {}, 136 | data() {}, 137 | props: {}, 138 | slots: {}, 139 | global: {}, 140 | shallow: true, 141 | localVue: null, 142 | store: {}, 143 | }, 144 | ['localVue', 'store'], 145 | ], 146 | ]), 147 | ], 148 | 149 | invalid: [ 150 | ...vtu1InvalidTests, 151 | ...makeFailingTestCases([ 152 | makeDeprecatedMountOptionTestCase('context'), 153 | makeDeprecatedMountOptionTestCase('listeners', 'props'), 154 | makeDeprecatedMountOptionTestCase('stubs', 'global.stubs'), 155 | makeDeprecatedMountOptionTestCase('mocks', 'global.mocks'), 156 | makeDeprecatedMountOptionTestCase('propsData', 'props'), 157 | makeDeprecatedMountOptionTestCase('provide', 'global.provide'), 158 | makeDeprecatedMountOptionTestCase('localVue', 'global'), 159 | makeDeprecatedMountOptionTestCase('scopedSlots', 'slots'), 160 | makeDeprecatedMountOptionTestCase('components', 'global.components'), 161 | makeDeprecatedMountOptionTestCase('directives', 'global.directives'), 162 | makeDeprecatedMountOptionTestCase('mixins', 'global.mixins'), 163 | makeDeprecatedMountOptionTestCase('store', 'global.plugins'), 164 | makeDeprecatedMountOptionTestCase('router', 'global.plugins'), 165 | ]), 166 | ], 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /src/rules/utils.js: -------------------------------------------------------------------------------- 1 | const functionNamesReturningWrappers = [ 2 | 'find', 3 | 'findAll', 4 | 'findComponent', 5 | 'findAllComponents', 6 | 'get', 7 | 'getComponent', 8 | 'at', 9 | ]; 10 | 11 | function nodeCalleeReturnsWrapper(node) { 12 | return ( 13 | node.type === 'CallExpression' && 14 | node.callee.type === 'MemberExpression' && 15 | node.callee.property.type === 'Identifier' && 16 | functionNamesReturningWrappers.includes(node.callee.property.name) 17 | ); 18 | } 19 | 20 | /** 21 | * Returns true if the memberObjectNode is an identifier with a name in the provided 22 | * list of `wrapperNames`, or is a function call which returns a wrapper and that function 23 | * call was chained off something which is a wrapper. 24 | * @returns {boolean} node was (probably) called from a wrapper 25 | */ 26 | function nodeIsCalledFromWrapper(memberObjectNode, wrapperNames) { 27 | // examples of memberObjectNode: the `wrapper.get(MyComponent)` in `wrapper.get(MyComponent).contains()` 28 | // examples of 'memberObjectNode.object: 29 | // the '1234' in '1234'.split() or the `wrapper` in `wrapper.get()` 30 | // examples of memberObjectNode.property: 31 | // things like the `['asdf']` in `foo['asdf']()`. or the `get` in `wrapper.get()` 32 | 33 | // this while loop checks that in a construct like `wrapper.get('div').contains()`, 34 | // `wrapper.contains()`, `[1, 2, 3].contains()`, etc: 35 | // - the expression is a function call 36 | // - that function call is a call of the form `AAAA(BBBB)` 37 | // - that `AAAA` is a member (property) access in the form of `xxxx.yyyy` 38 | // - that `yyyy` is an identifier who's name matches one of the ones which returns another wrapper 39 | // then, the loop repeats on `xxxx`, which may itself be another chained function call of the form AAAA(BBBB) 40 | // at the end of the loop, we know that we have arrived at something which is either: 41 | // - an identifier which started the chain, OR 42 | // - something else which is definitely not a wrapper 43 | while (nodeCalleeReturnsWrapper(memberObjectNode)) { 44 | memberObjectNode = memberObjectNode.callee.object; 45 | } 46 | 47 | // checks that the final `xxxx` from the steps above is an identifier which matches one of the valid 48 | // passed in wrapper names 49 | // (eg, confirms that the root of this function call started at a variable named 'wrapper') 50 | if (memberObjectNode.type === 'Identifier' && wrapperNames.includes(memberObjectNode.name)) { 51 | return true; 52 | } 53 | 54 | // on all other constructs, decide this is not a wrapper. 55 | return false; 56 | } 57 | 58 | /** 59 | * Returns true if the node represents a function call which is triggering a custom component 60 | * emit. Eg, `wrapper.getComponent(MyComponent).vm.$emit('....')` returns true. 61 | * @param {*} node 62 | * @returns {boolean} 63 | */ 64 | function nodeIsComponentEmit(node) { 65 | return ( 66 | node.callee && 67 | node.callee.type === 'MemberExpression' && 68 | node.callee.object.type === 'MemberExpression' && 69 | node.callee.object.property.name === 'vm' && 70 | node.callee.property.name === '$emit' 71 | ); 72 | } 73 | 74 | function resolveIdentifierToVariable(identifierNode, scope) { 75 | if (identifierNode.type !== 'Identifier') { 76 | return null; 77 | } 78 | 79 | while (scope) { 80 | const boundIdentifier = scope.variables.find(({ name }) => name === identifierNode.name); 81 | if (boundIdentifier) { 82 | return boundIdentifier; 83 | } 84 | scope = scope.upper; 85 | } 86 | 87 | return null; 88 | } 89 | 90 | function getImportSourceName(boundIdentifier) { 91 | const importDefinition = boundIdentifier.defs.find(({ type }) => type === 'ImportBinding'); 92 | return importDefinition.node.parent.source.value; 93 | } 94 | 95 | function isComponentImport(importSourceName, identifierName, filename) { 96 | let resolvedModulePath; 97 | try { 98 | resolvedModulePath = require.resolve(importSourceName, { paths: [filename] }); 99 | } catch { 100 | return false; // install your packages, heathens! 101 | } 102 | 103 | try { 104 | // require() does not take { paths } argument, so need to pass resolved filename directly 105 | const m = require(resolvedModulePath); 106 | const imported = identifierName ? m[identifierName] : m; 107 | 108 | // close enough, right? 109 | return typeof imported === 'object' || typeof imported === 'function'; 110 | } catch { 111 | // some packages can't be imported in node. fall back to a secondary detection. 112 | // it isn't perfect but it should work pretty okay for most working tests and vue libraries. 113 | return resolvedModulePath.includes('node_modules') && importSourceName.includes('vue'); 114 | } 115 | } 116 | 117 | /** 118 | * 119 | * @param {*} node 120 | * @param {import('eslint').Rule.RuleContext} context 121 | * @returns 122 | */ 123 | function isComponentSelector(node, context) { 124 | if (node.type === 'ObjectExpression') { 125 | return true; 126 | } 127 | 128 | const boundIdentifier = resolveIdentifierToVariable(node, context.getScope()); 129 | if (!boundIdentifier) { 130 | return false; 131 | } 132 | 133 | const importDefinition = boundIdentifier.defs.find(({ type }) => type === 'ImportBinding'); 134 | if (importDefinition) { 135 | const importedName = 136 | importDefinition.node.type === 'ImportSpecifier' 137 | ? importDefinition.node.imported.name 138 | : /* default import */ undefined; 139 | const importSourceName = getImportSourceName(boundIdentifier); 140 | 141 | const isVueSourceFileImport = importSourceName.endsWith('.vue'); 142 | 143 | // short circuit to avoid costly module resolution attempts 144 | return ( 145 | isVueSourceFileImport || 146 | isComponentImport( 147 | importSourceName, 148 | importedName, 149 | // is a special value indicating the input came from stdin 150 | context.getFilename() === '' ? context.getCwd() : context.getFilename() 151 | ) 152 | ); 153 | } 154 | 155 | // note(@alexv): could potentially add logic here to check for object literal assignment, require(), or mount() 156 | return false; 157 | } 158 | 159 | function isVtuImport(identifierNode, scope) { 160 | const boundIdentifier = resolveIdentifierToVariable(identifierNode, scope); 161 | if (!boundIdentifier) { 162 | return false; 163 | } 164 | return getImportSourceName(boundIdentifier) === '@vue/test-utils'; 165 | } 166 | 167 | module.exports = { 168 | nodeCalleeReturnsWrapper, 169 | nodeIsCalledFromWrapper, 170 | nodeIsComponentEmit, 171 | resolveIdentifierToVariable, 172 | isComponentSelector, 173 | isVtuImport, 174 | }; 175 | -------------------------------------------------------------------------------- /src/rules/no-deprecated-selectors.js: -------------------------------------------------------------------------------- 1 | const { get } = require('lodash'); 2 | const path = require('path'); 3 | const isVtuVersionAtLeast = require('./checkVtuVersion'); 4 | const { VTU_PLUGIN_SETTINGS_KEY } = require('./constants'); 5 | const { nodeIsCalledFromWrapper, nodeCalleeReturnsWrapper, isComponentSelector } = require('./utils'); 6 | const { detectVtuVersion } = isVtuVersionAtLeast; 7 | 8 | const DEFAULT_WRAPPER_VARIABLES = ['wrapper']; 9 | 10 | /** 11 | * @type {import('eslint').Rule.RuleModule} 12 | */ 13 | module.exports = { 14 | meta: { 15 | type: 'problem', 16 | docs: { 17 | description: 'disallow deprecated selector usage', 18 | url: path.join(__dirname, '../../docs/rules/no-deprecated-selectors.md'), 19 | }, 20 | fixable: 'code', 21 | schema: [ 22 | { 23 | type: 'object', 24 | properties: { 25 | wrapperNames: { 26 | description: 'List of variable names to which wrappers are typically assigned', 27 | type: 'array', 28 | items: { 29 | type: 'string', 30 | }, 31 | }, 32 | }, 33 | }, 34 | ], // Add a schema if the rule has options 35 | messages: { 36 | deprecatedComponentSelector: 37 | 'Calling {{ functionName }} with a component selector is deprecated and will be removed in VTU 2.', 38 | memberUsageFromDeprecatedSelector: 39 | '{{ functionName }} will no longer return `wrapper.{{ missingMemberName }}` in VTU 2. Use {{ alternateFunctionName }} with a component selector instead.', 40 | }, 41 | }, 42 | 43 | create(context) { 44 | const wrapperNames = (context.options[0] && context.options[0].wrapperNames) || DEFAULT_WRAPPER_VARIABLES; 45 | const vtuVersion = get(context.settings, [VTU_PLUGIN_SETTINGS_KEY, 'version']) || detectVtuVersion(); 46 | 47 | const componentOnlyWrapperMembers = new Set(['vm', 'props', 'setData', 'setProps', 'emitted']); 48 | 49 | const deprecatedComponentSelectorFunctions = { 50 | // functionName => preferred name 51 | find: 'findComponent', 52 | findAll: 'findAllComponents', 53 | get: 'getComponent', 54 | }; 55 | 56 | const canChainComponentsFromCssWrappers = isVtuVersionAtLeast(vtuVersion, '1.3.0'); 57 | 58 | return { 59 | CallExpression(node) { 60 | if (node.callee.type !== 'MemberExpression' || node.callee.property.type !== 'Identifier') { 61 | return; 62 | } 63 | 64 | if ( 65 | node.callee.property.name in deprecatedComponentSelectorFunctions && 66 | nodeIsCalledFromWrapper(node.callee.object, wrapperNames) 67 | ) { 68 | // these functions should always have strings passed to them, never objects or components 69 | if (node.arguments[0] && isComponentSelector(node.arguments[0], context)) { 70 | let isSuccessiveWrapperChain = false; 71 | let wrapperSelectorCall = node.callee.object; 72 | while (nodeCalleeReturnsWrapper(wrapperSelectorCall)) { 73 | if (wrapperSelectorCall.callee.property.name in deprecatedComponentSelectorFunctions) { 74 | // cannot autofix in versions before 1.3 because this is a chain like `get('div').get(SomeComponent)`. 75 | // Autofixing to `get('div').getComponent(SomeComponent)` will cause an error on those versions 76 | isSuccessiveWrapperChain = true; 77 | break; 78 | } 79 | wrapperSelectorCall = wrapperSelectorCall.callee.object; 80 | } 81 | 82 | context.report({ 83 | messageId: 'deprecatedComponentSelector', 84 | node: node.arguments[0], 85 | data: { 86 | functionName: node.callee.property.name, 87 | }, 88 | fix: 89 | isSuccessiveWrapperChain && !canChainComponentsFromCssWrappers 90 | ? undefined 91 | : fixer => { 92 | return fixer.replaceText( 93 | node.callee.property, 94 | deprecatedComponentSelectorFunctions[node.callee.property.name] 95 | ); 96 | }, 97 | }); 98 | return; 99 | } 100 | } 101 | }, 102 | MemberExpression(node) { 103 | if ( 104 | node.property.type === 'Identifier' && 105 | componentOnlyWrapperMembers.has(node.property.name) && 106 | nodeIsCalledFromWrapper(node.object, wrapperNames) && 107 | nodeCalleeReturnsWrapper(node.object) // if object isn't a call which returns wrapper, then member usage is rooted directly off wrapper which is safe 108 | ) { 109 | // the member usage should be not be chained immediately after a non-component selector function 110 | // (okay if previous calls don't return components as long as the last one does) 111 | let lastWrapperCall = node.object; 112 | if ( 113 | lastWrapperCall.callee.property.name === 'at' && 114 | nodeCalleeReturnsWrapper(lastWrapperCall.callee.object) 115 | ) { 116 | // special handling for findAll().at().foo - need to make sure we're looking at the 'findAll', 117 | // not the 'at' 118 | lastWrapperCall = lastWrapperCall.callee.object; 119 | } 120 | if (lastWrapperCall.callee.property.name in deprecatedComponentSelectorFunctions) { 121 | context.report({ 122 | messageId: 'memberUsageFromDeprecatedSelector', 123 | node, 124 | data: { 125 | functionName: lastWrapperCall.callee.property.name, 126 | missingMemberName: node.property.name, 127 | alternateFunctionName: 128 | deprecatedComponentSelectorFunctions[lastWrapperCall.callee.property.name], 129 | }, 130 | }); 131 | return; 132 | } 133 | } 134 | }, 135 | }; 136 | }, 137 | }; 138 | -------------------------------------------------------------------------------- /tests/src/rules/no-deprecated-selectors.test.js: -------------------------------------------------------------------------------- 1 | const { flatMap } = require('lodash'); 2 | 3 | const rule = require('../../../src/rules/no-deprecated-selectors'), 4 | RuleTester = require('eslint').RuleTester; 5 | 6 | const componentOnlyWrapperMembers = ['vm', 'props', 'setData', 'setProps', 'emitted']; 7 | 8 | describe('version independent tests', () => { 9 | const ruleTester = new RuleTester({ 10 | parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, 11 | settings: { vtu: { version: '1.2.0' } }, 12 | }); 13 | ruleTester.run('no-deprecated-selectors', rule, { 14 | valid: [ 15 | { code: 'import MyComponent from "./MyComponent.vue"; wrapper.findAllComponents(MyComponent)' }, 16 | { code: 'import MyComponent from "./MyComponent.vue"; wrapper.findComponent(MyComponent)' }, 17 | { code: 'wrapper.findAllComponents({ name: "MyComponent" })' }, 18 | { code: 'wrapper.findComponent({ name: "MyComponent" })' }, 19 | { code: "wrapper.findAll('div')" }, 20 | { code: "wrapper.find('div')" }, 21 | { code: "wrapper.get('div')" }, 22 | { code: "const button = 'button'; wrapper.get(button);" }, 23 | { code: 'import MyComponent from "./MyComponent.vue"; wrapper.getComponent(MyComponent)' }, 24 | { code: 'wrapper.getComponent({ name: "MyComponent" })' }, 25 | { code: 'wrapper.vm' }, 26 | ...flatMap(componentOnlyWrapperMembers, member => [ 27 | { code: `wrapper.getComponent("div").${member}` }, 28 | { code: `wrapper.get("div").getComponent("div").${member}` }, 29 | { code: `wrapper.findAllComponents("div").at(0).${member}` }, 30 | ]), 31 | { 32 | // normally illegal but passes because of options 33 | code: 'import MyComponent from "./MyComponent.vue"; wrapper.find(MyComponent)', 34 | options: [{ wrapperNames: ['foo'] }], 35 | }, 36 | ], 37 | 38 | invalid: [ 39 | { 40 | code: 'import MyComponent from "./MyComponent.vue"; wrapper.find(MyComponent)', 41 | errors: [{ messageId: 'deprecatedComponentSelector' }], 42 | output: 'import MyComponent from "./MyComponent.vue"; wrapper.findComponent(MyComponent)', 43 | }, 44 | { 45 | code: "wrapper.find({ name: 'MyComponent' })", 46 | errors: [{ messageId: 'deprecatedComponentSelector' }], 47 | output: "wrapper.findComponent({ name: 'MyComponent' })", 48 | }, 49 | { 50 | // chained functions 51 | code: "import MyComponent from './MyComponent.vue'; wrapper.get(MyComponent).contains('div')", 52 | errors: [{ messageId: 'deprecatedComponentSelector', data: { functionName: 'get' } }], 53 | output: "import MyComponent from './MyComponent.vue'; wrapper.getComponent(MyComponent).contains('div')", 54 | }, 55 | { 56 | // chained functions with at() 57 | code: "import MyComponent from './MyComponent.vue'; wrapper.findAll(MyComponent).at(2).contains('div')", 58 | errors: [{ messageId: 'deprecatedComponentSelector', data: { functionName: 'findAll' } }], 59 | output: "import MyComponent from './MyComponent.vue'; wrapper.findAllComponents(MyComponent).at(2).contains('div')", 60 | }, 61 | ...flatMap(componentOnlyWrapperMembers, member => [ 62 | { 63 | // member usage off non-component selector function 64 | code: `wrapper.get('div').${member}`, 65 | errors: [ 66 | { 67 | messageId: 'memberUsageFromDeprecatedSelector', 68 | data: { 69 | functionName: 'get', 70 | alternateFunctionName: 'getComponent', 71 | missingMemberName: member, 72 | }, 73 | }, 74 | ], 75 | output: `wrapper.get('div').${member}`, 76 | }, 77 | { 78 | // member usage off non-component findAll().at() 79 | code: `wrapper.findAll('div').at(0).${member}`, 80 | errors: [ 81 | { 82 | messageId: 'memberUsageFromDeprecatedSelector', 83 | data: { 84 | functionName: 'findAll', 85 | alternateFunctionName: 'findAllComponents', 86 | missingMemberName: member, 87 | }, 88 | }, 89 | ], 90 | output: `wrapper.findAll('div').at(0).${member}`, 91 | }, 92 | { 93 | // chained member usage off non-component wrapper getter 94 | code: `wrapper.get('div').get('div').${member}`, 95 | errors: [ 96 | { 97 | messageId: 'memberUsageFromDeprecatedSelector', 98 | data: { 99 | functionName: 'get', 100 | alternateFunctionName: 'getComponent', 101 | missingMemberName: member, 102 | }, 103 | }, 104 | ], 105 | output: `wrapper.get('div').get('div').${member}`, 106 | }, 107 | ]), 108 | { 109 | // wrapperNames option 110 | code: 'import MyComponent from "./MyComponent.vue"; foo.find(MyComponent)', 111 | options: [{ wrapperNames: ['foo'] }], 112 | errors: [{ messageId: 'deprecatedComponentSelector' }], 113 | output: 'import MyComponent from "./MyComponent.vue"; foo.findComponent(MyComponent)', 114 | }, 115 | ], 116 | }); 117 | }); 118 | 119 | describe('version 1.2.2', () => { 120 | const ruleTester = new RuleTester({ 121 | parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, 122 | settings: { vtu: { version: '1.2.2' } }, 123 | }); 124 | ruleTester.run('no-deprecated-selectors', rule, { 125 | valid: [{ code: 'wrapper.get("div").getComponent("div").vm' }], 126 | 127 | invalid: [ 128 | { 129 | // deprecated selector chained off DOM selector cannot be autofixed until 1.3.0 130 | code: "import MyComponent from './MyComponent.vue'; wrapper.get('div').get(MyComponent)", 131 | errors: [ 132 | { 133 | messageId: 'deprecatedComponentSelector', 134 | data: { functionName: 'get' }, 135 | }, 136 | ], 137 | output: "import MyComponent from './MyComponent.vue'; wrapper.get('div').get(MyComponent)", 138 | }, 139 | ], 140 | }); 141 | }); 142 | 143 | describe('version 1.3.0', () => { 144 | const ruleTester = new RuleTester({ 145 | parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, 146 | settings: { vtu: { version: '1.3.0' } }, 147 | }); 148 | ruleTester.run('no-deprecated-selectors', rule, { 149 | valid: [{ code: 'wrapper.get("div").getComponent("div").vm' }], 150 | 151 | invalid: [ 152 | { 153 | // deprecated selector chained off DOM selector can be autofixed in vtu 1.3.0 154 | code: "import MyComponent from './MyComponent.vue'; wrapper.get('div').get(MyComponent)", 155 | errors: [ 156 | { 157 | messageId: 'deprecatedComponentSelector', 158 | data: { functionName: 'get' }, 159 | }, 160 | ], 161 | output: "import MyComponent from './MyComponent.vue'; wrapper.get('div').getComponent(MyComponent)", 162 | }, 163 | ], 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /src/rules/no-deprecated-mount-options.js: -------------------------------------------------------------------------------- 1 | const { get } = require('lodash'); 2 | const path = require('path'); 3 | const isVtuVersionAtLeast = require('./checkVtuVersion'); 4 | const { VTU_PLUGIN_SETTINGS_KEY } = require('./constants'); 5 | const { isVtuImport } = require('./utils'); 6 | const { detectVtuVersion } = isVtuVersionAtLeast; 7 | 8 | /** 9 | * @type {import('eslint').Rule.RuleModule} 10 | */ 11 | module.exports = { 12 | meta: { 13 | type: 'problem', 14 | docs: { 15 | description: 'disallow deprecated mount options', 16 | url: path.join(__dirname, '../../docs/rules/no-deprecated-mount-options.md'), 17 | }, 18 | fixable: 'code', 19 | schema: [ 20 | { 21 | type: 'object', 22 | properties: { 23 | ignoreMountOptions: { 24 | description: 'List of mount option property names to ignore', 25 | type: 'array', 26 | items: { 27 | type: 'string', 28 | }, 29 | }, 30 | }, 31 | }, 32 | ], // Add a schema if the rule has options 33 | messages: { 34 | deprecatedMountOption: 35 | 'The mount option `{{ mountOption }}` is deprecated and will be removed in VTU 2.{{ replacementOption }}', 36 | unknownMountOption: 37 | 'The mount option `{{ mountOption }}` is relying on component option merging and will have no effect in VTU 2.', 38 | syncIsRemoved: 'The mount option `sync` was removed in VTU 1.0.0-beta.30 and has no effect.', 39 | }, 40 | }, 41 | 42 | create(context) { 43 | const allowedMountOptions = (context.options[0] && context.options[0].ignoreMountOptions) || []; 44 | const vtuVersion = get(context.settings, [VTU_PLUGIN_SETTINGS_KEY, 'version']) || detectVtuVersion(); 45 | 46 | const sourceCode = context.getSourceCode(); 47 | 48 | const isComma = token => { 49 | return token.type === 'Punctuator' && token.value === ','; 50 | }; 51 | 52 | const getPropertyName = property => 53 | property.key.type === 'Identifier' ? property.key.name : property.key.value; 54 | 55 | function deleteProperty(/** @type {import('eslint').Rule.RuleFixer} */ fixer, property) { 56 | const afterProperty = sourceCode.getTokenAfter(property); 57 | const hasComma = isComma(afterProperty); 58 | 59 | return fixer.removeRange([property.range[0], hasComma ? afterProperty.range[1] : property.range[1]]); 60 | } 61 | 62 | const isVtu2 = isVtuVersionAtLeast(vtuVersion, '2.0.0'); 63 | 64 | const removedOptions = { 65 | // deprecated or replaceable in vtu 1/vue 2 66 | attachToDocument: { 67 | replacementOption: 'attachTo', 68 | fixer: (/** @type {import('eslint').Rule.RuleFixer} */ fixer, property) => [ 69 | fixer.replaceText(property.key, 'attachTo'), 70 | fixer.replaceText(property.value, 'document.body'), 71 | ], 72 | }, 73 | parentComponent: null, 74 | filters: null, 75 | 76 | // removed or moved in vtu 2 77 | context: null, 78 | listeners: { 79 | replacementOption: 'props', 80 | }, 81 | stubs: { 82 | replacementOption: 'global.stubs', 83 | }, 84 | mocks: { 85 | replacementOption: 'global.mocks', 86 | }, 87 | propsData: { 88 | replacementOption: 'props', 89 | }, 90 | provide: { 91 | replacementOption: 'global.provide', 92 | }, 93 | localVue: { 94 | replacementOption: 'global', 95 | }, 96 | scopedSlots: { 97 | replacementOption: 'slots', 98 | }, 99 | 100 | // not explicitly removed but has trivial replacement 101 | components: { 102 | replacementOption: 'global.components', 103 | }, 104 | directives: { 105 | replacementOption: 'global.directives', 106 | }, 107 | mixins: { 108 | replacementOption: 'global.mixins', 109 | }, 110 | store: { 111 | replacementOption: 'global.plugins', 112 | }, 113 | router: { 114 | replacementOption: 'global.plugins', 115 | }, 116 | }; 117 | 118 | const knownValidMountOptions = isVtu2 119 | ? new Set(['attachTo', 'attrs', 'data', 'props', 'slots', 'global', 'shallow']) 120 | : new Set([ 121 | 'context', 122 | 'data', 123 | 'slots', 124 | 'scopedSlots', 125 | 'stubs', 126 | 'mocks', 127 | 'localVue', 128 | 'attachTo', 129 | 'attrs', 130 | 'propsData', 131 | 'provide', 132 | 'listeners', 133 | 134 | // these properties technically rely on configuration merging 135 | // with the underlying component but are common practice and 136 | // have an autofixable replacement in VTU 2 137 | 'components', 138 | 'directives', 139 | 'mixins', 140 | 'store', 141 | 'router', 142 | ]); 143 | // add user-whitelisted options 144 | allowedMountOptions.forEach(opt => knownValidMountOptions.add(opt)); 145 | 146 | const mountFunctionNames = new Set(['mount', 'shallowMount']); 147 | 148 | return { 149 | CallExpression(node) { 150 | if (node.callee.type !== 'Identifier' || !mountFunctionNames.has(node.callee.name)) { 151 | return; 152 | } 153 | if (!isVtuImport(node.callee, context.getScope())) { 154 | return; 155 | } 156 | 157 | const mountOptionsNode = node.arguments[1]; 158 | if (!mountOptionsNode || mountOptionsNode.type !== 'ObjectExpression') { 159 | // second argument is not object literal 160 | return; 161 | } 162 | 163 | // filter out object spreads 164 | /** @type {import('estree').Property[]} */ 165 | const properties = mountOptionsNode.properties.filter(({ type }) => type === 'Property'); 166 | 167 | properties.forEach(property => { 168 | if (property.key.type !== 'Identifier' && property.key.type !== 'Literal') { 169 | return; 170 | } 171 | const keyName = getPropertyName(property); 172 | if (keyName === 'sync' && isVtuVersionAtLeast(vtuVersion, '1.0.0-beta.30')) { 173 | context.report({ 174 | messageId: 'syncIsRemoved', 175 | node: property, 176 | fix(fixer) { 177 | return deleteProperty(fixer, property); 178 | }, 179 | }); 180 | } else if (!knownValidMountOptions.has(keyName)) { 181 | context.report({ 182 | messageId: !(keyName in removedOptions) ? 'unknownMountOption' : 'deprecatedMountOption', 183 | node: property, 184 | fix: 185 | removedOptions[keyName] && removedOptions[keyName].fixer 186 | ? fixer => removedOptions[keyName].fixer(fixer, property, mountOptionsNode) 187 | : undefined, 188 | data: { 189 | mountOption: keyName, 190 | replacementOption: removedOptions[keyName] 191 | ? ` Use '${removedOptions[keyName].replacementOption}' instead.` 192 | : '', 193 | }, 194 | }); 195 | } 196 | }); 197 | }, 198 | }; 199 | }, 200 | }; 201 | --------------------------------------------------------------------------------