diff --git a/.eslintrc.json b/.eslintrc.json index f04ce17a..f2555e8d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,6 +23,7 @@ }, "rules": { "no-var": "error", - "@typescript-eslint/explicit-function-return-type": "off" + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }] } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca86b31a..c78adbaa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,11 +63,6 @@ each rule has three files named with its identifier (e.g. `no-debug`): Additionally, you need to do a couple of extra things: -- Import the new rule in `lib/index.ts` and include it - in `rules` constant (there is a test which will make sure you did - this). Remember to include your rule under corresponding `config` if necessary - (a snapshot test will check this too, but you can update it just running - `npm run test:update`). - Include your rule in the "Supported Rules" table within the [README.md](./README.md). Don't forget to include the proper badges if needed and to sort alphabetically the rules for readability. @@ -105,6 +100,8 @@ If you need some check related to Testing Library which is not available in any - pass it through `helpers` - write some generic test within `fake-rule.ts`, which is a dumb rule to be able to test all enhanced behavior from our custom Rule Creator. +Take also into account that we're using our own `recommendedConfig` meta instead of the default `recommended` one. This is done so that our tools can automatically generate (`npm run generate:configs`) our configs. + ## Updating existing rules A couple of things you need to remember when editing already existing rules: diff --git a/lib/configs/angular.ts b/lib/configs/angular.ts new file mode 100644 index 00000000..671b496b --- /dev/null +++ b/lib/configs/angular.ts @@ -0,0 +1,21 @@ +// THIS CODE WAS AUTOMATICALLY GENERATED +// DO NOT EDIT THIS CODE BY HAND +// YOU CAN REGENERATE IT USING npm run generate:configs + +export = { + plugins: ['testing-library'], + rules: { + 'testing-library/await-async-query': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-container': 'error', + 'testing-library/no-debug': 'error', + 'testing-library/no-dom-import': ['error', 'angular'], + 'testing-library/no-node-access': 'error', + 'testing-library/no-promise-in-fire-event': 'error', + 'testing-library/no-wait-for-empty-callback': 'error', + 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-screen-queries': 'error', + 'testing-library/render-result-naming-convention': 'error', + }, +}; diff --git a/lib/configs/dom.ts b/lib/configs/dom.ts new file mode 100644 index 00000000..261cde83 --- /dev/null +++ b/lib/configs/dom.ts @@ -0,0 +1,16 @@ +// THIS CODE WAS AUTOMATICALLY GENERATED +// DO NOT EDIT THIS CODE BY HAND +// YOU CAN REGENERATE IT USING npm run generate:configs + +export = { + plugins: ['testing-library'], + rules: { + 'testing-library/await-async-query': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-promise-in-fire-event': 'error', + 'testing-library/no-wait-for-empty-callback': 'error', + 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-screen-queries': 'error', + }, +}; diff --git a/lib/configs/index.ts b/lib/configs/index.ts new file mode 100644 index 00000000..15b63782 --- /dev/null +++ b/lib/configs/index.ts @@ -0,0 +1,24 @@ +import { join } from 'path'; + +import type { TSESLint } from '@typescript-eslint/experimental-utils'; + +import { + importDefault, + SUPPORTED_TESTING_FRAMEWORKS, + SupportedTestingFramework, +} from '../utils'; + +export type LinterConfigRules = Record; + +const configsDir = __dirname; + +const getConfigForFramework = (framework: SupportedTestingFramework) => + importDefault(join(configsDir, framework)); + +export default SUPPORTED_TESTING_FRAMEWORKS.reduce( + (allConfigs, framework) => ({ + ...allConfigs, + [framework]: getConfigForFramework(framework), + }), + {} +) as Record; diff --git a/lib/configs/react.ts b/lib/configs/react.ts new file mode 100644 index 00000000..b2161875 --- /dev/null +++ b/lib/configs/react.ts @@ -0,0 +1,21 @@ +// THIS CODE WAS AUTOMATICALLY GENERATED +// DO NOT EDIT THIS CODE BY HAND +// YOU CAN REGENERATE IT USING npm run generate:configs + +export = { + plugins: ['testing-library'], + rules: { + 'testing-library/await-async-query': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-container': 'error', + 'testing-library/no-debug': 'error', + 'testing-library/no-dom-import': ['error', 'react'], + 'testing-library/no-node-access': 'error', + 'testing-library/no-promise-in-fire-event': 'error', + 'testing-library/no-wait-for-empty-callback': 'error', + 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-screen-queries': 'error', + 'testing-library/render-result-naming-convention': 'error', + }, +}; diff --git a/lib/configs/vue.ts b/lib/configs/vue.ts new file mode 100644 index 00000000..046bb774 --- /dev/null +++ b/lib/configs/vue.ts @@ -0,0 +1,22 @@ +// THIS CODE WAS AUTOMATICALLY GENERATED +// DO NOT EDIT THIS CODE BY HAND +// YOU CAN REGENERATE IT USING npm run generate:configs + +export = { + plugins: ['testing-library'], + rules: { + 'testing-library/await-async-query': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/await-fire-event': 'error', + 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-container': 'error', + 'testing-library/no-debug': 'error', + 'testing-library/no-dom-import': ['error', 'vue'], + 'testing-library/no-node-access': 'error', + 'testing-library/no-promise-in-fire-event': 'error', + 'testing-library/no-wait-for-empty-callback': 'error', + 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-screen-queries': 'error', + 'testing-library/render-result-naming-convention': 'error', + }, +}; diff --git a/lib/create-testing-library-rule/index.ts b/lib/create-testing-library-rule/index.ts index fda6ad26..9ec86214 100644 --- a/lib/create-testing-library-rule/index.ts +++ b/lib/create-testing-library-rule/index.ts @@ -1,6 +1,6 @@ import { ESLintUtils, TSESLint } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; +import { getDocsUrl, TestingLibraryRuleMeta } from '../utils'; import { DetectionOptions, @@ -8,12 +8,6 @@ import { EnhancedRuleCreate, } from './detect-testing-library-utils'; -// These 2 types are copied from @typescript-eslint/experimental-utils -type CreateRuleMetaDocs = Omit; -type CreateRuleMeta = { - docs: CreateRuleMetaDocs; -} & Omit, 'docs'>; - export function createTestingLibraryRule< TOptions extends readonly unknown[], TMessageIds extends string, @@ -21,10 +15,11 @@ export function createTestingLibraryRule< >({ create, detectionOptions = {}, + meta, ...remainingConfig }: Readonly<{ name: string; - meta: CreateRuleMeta; + meta: TestingLibraryRuleMeta; defaultOptions: Readonly; detectionOptions?: Partial; create: EnhancedRuleCreate; @@ -35,5 +30,14 @@ export function createTestingLibraryRule< create, detectionOptions ), + meta: { + ...meta, + docs: { + ...meta.docs, + // We're using our own recommendedConfig meta to tell our build tools + // if the rule is recommended on a config basis + recommended: false, + }, + }, }); } diff --git a/lib/index.ts b/lib/index.ts index 661a7129..e288411a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,61 +1,7 @@ +import configs from './configs'; import rules from './rules'; -const domRules = { - 'testing-library/await-async-query': 'error', - 'testing-library/await-async-utils': 'error', - 'testing-library/no-await-sync-query': 'error', - 'testing-library/no-promise-in-fire-event': 'error', - 'testing-library/no-wait-for-empty-callback': 'error', - 'testing-library/prefer-find-by': 'error', - 'testing-library/prefer-screen-queries': 'error', -}; - -const angularRules = { - ...domRules, - 'testing-library/no-container': 'error', - 'testing-library/no-debug': 'error', - 'testing-library/no-dom-import': ['error', 'angular'], - 'testing-library/no-node-access': 'error', - 'testing-library/render-result-naming-convention': 'error', -}; - -const reactRules = { - ...domRules, - 'testing-library/no-container': 'error', - 'testing-library/no-debug': 'error', - 'testing-library/no-dom-import': ['error', 'react'], - 'testing-library/no-node-access': 'error', - 'testing-library/render-result-naming-convention': 'error', -}; - -const vueRules = { - ...domRules, - 'testing-library/await-fire-event': 'error', - 'testing-library/no-container': 'error', - 'testing-library/no-debug': 'error', - 'testing-library/no-dom-import': ['error', 'vue'], - 'testing-library/no-node-access': 'error', - 'testing-library/render-result-naming-convention': 'error', -}; - export = { + configs, rules, - configs: { - dom: { - plugins: ['testing-library'], - rules: domRules, - }, - angular: { - plugins: ['testing-library'], - rules: angularRules, - }, - react: { - plugins: ['testing-library'], - rules: reactRules, - }, - vue: { - plugins: ['testing-library'], - rules: vueRules, - }, - }, }; diff --git a/lib/rules/await-async-query.ts b/lib/rules/await-async-query.ts index 6acf99ca..0e864e65 100644 --- a/lib/rules/await-async-query.ts +++ b/lib/rules/await-async-query.ts @@ -19,7 +19,12 @@ export default createTestingLibraryRule({ docs: { description: 'Enforce promises from async queries to be handled', category: 'Best Practices', - recommended: 'warn', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + }, }, messages: { awaitAsyncQuery: 'promise returned from {{ name }} query must be handled', diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index c4e40373..a220fe90 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -19,7 +19,12 @@ export default createTestingLibraryRule({ docs: { description: 'Enforce promises from async utils to be handled', category: 'Best Practices', - recommended: 'warn', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + }, }, messages: { awaitAsyncUtil: 'Promise returned from `{{ name }}` must be handled', diff --git a/lib/rules/await-fire-event.ts b/lib/rules/await-fire-event.ts index 7f635bc8..f5ff5f61 100644 --- a/lib/rules/await-fire-event.ts +++ b/lib/rules/await-fire-event.ts @@ -19,7 +19,12 @@ export default createTestingLibraryRule({ docs: { description: 'Enforce promises from fire event methods to be handled', category: 'Best Practices', - recommended: false, + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: 'error', + }, }, messages: { awaitFireEvent: diff --git a/lib/rules/consistent-data-testid.ts b/lib/rules/consistent-data-testid.ts index 63a9b888..6b17ad5b 100644 --- a/lib/rules/consistent-data-testid.ts +++ b/lib/rules/consistent-data-testid.ts @@ -19,7 +19,12 @@ export default createTestingLibraryRule({ docs: { description: 'Ensures consistent usage of `data-testid`', category: 'Best Practices', - recommended: false, + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { consistentDataTestId: '`{{attr}}` "{{value}}" should match `{{regex}}`', diff --git a/lib/rules/index.ts b/lib/rules/index.ts index 5229c62f..1a1e79f1 100644 --- a/lib/rules/index.ts +++ b/lib/rules/index.ts @@ -3,16 +3,13 @@ import { join, parse } from 'path'; import { TSESLint } from '@typescript-eslint/experimental-utils'; -type RuleModule = TSESLint.RuleModule; +import { importDefault, TestingLibraryRuleMeta } from '../utils'; -// Copied from https://github.com/babel/babel/blob/b35c78f08dd854b08575fc66ebca323fdbc59dab/packages/babel-helpers/src/helpers.js#L615-L619 -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const interopRequireDefault = (obj: any): { default: unknown } => - obj?.__esModule ? obj : { default: obj }; - -const importDefault = (moduleName: string) => - // eslint-disable-next-line @typescript-eslint/no-var-requires - interopRequireDefault(require(moduleName)).default; +type RuleModule = TSESLint.RuleModule & { + meta: TestingLibraryRuleMeta & { + recommended: false; + }; +}; const rulesDir = __dirname; const excludedFiles = ['index']; @@ -23,7 +20,7 @@ export default readdirSync(rulesDir) .reduce>( (allRules, ruleName) => ({ ...allRules, - [ruleName]: importDefault(join(rulesDir, ruleName)) as RuleModule, + [ruleName]: importDefault(join(rulesDir, ruleName)), }), {} ); diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index 1e5e4770..1080336a 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -21,7 +21,12 @@ export default createTestingLibraryRule({ docs: { description: 'Disallow unnecessary `await` for sync events', category: 'Best Practices', - recommended: 'error', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { noAwaitSyncEvents: diff --git a/lib/rules/no-await-sync-query.ts b/lib/rules/no-await-sync-query.ts index f3604ce9..ce484159 100644 --- a/lib/rules/no-await-sync-query.ts +++ b/lib/rules/no-await-sync-query.ts @@ -13,7 +13,12 @@ export default createTestingLibraryRule({ docs: { description: 'Disallow unnecessary `await` for sync queries', category: 'Best Practices', - recommended: 'error', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + }, }, messages: { noAwaitSyncQuery: diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 781b8762..09d63524 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -20,7 +20,12 @@ export default createTestingLibraryRule({ docs: { description: 'Disallow the use of container methods', category: 'Best Practices', - recommended: 'error', + recommendedConfig: { + dom: false, + angular: 'error', + react: 'error', + vue: 'error', + }, }, messages: { noContainer: diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index 71e0f8f1..d1a492cf 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -22,7 +22,12 @@ export default createTestingLibraryRule({ docs: { description: 'Disallow unnecessary debug usages in the tests', category: 'Best Practices', - recommended: 'warn', + recommendedConfig: { + dom: false, + angular: 'error', + react: 'error', + vue: 'error', + }, }, messages: { noDebug: 'Unexpected debug statement', diff --git a/lib/rules/no-dom-import.ts b/lib/rules/no-dom-import.ts index 1246447d..f294dd8c 100644 --- a/lib/rules/no-dom-import.ts +++ b/lib/rules/no-dom-import.ts @@ -18,7 +18,12 @@ export default createTestingLibraryRule({ docs: { description: 'Disallow importing from DOM Testing Library', category: 'Best Practices', - recommended: false, + recommendedConfig: { + dom: false, + angular: ['error', 'angular'], + react: ['error', 'react'], + vue: ['error', 'vue'], + }, }, messages: { noDomImport: diff --git a/lib/rules/no-manual-cleanup.ts b/lib/rules/no-manual-cleanup.ts index a49a9836..7fdad94d 100644 --- a/lib/rules/no-manual-cleanup.ts +++ b/lib/rules/no-manual-cleanup.ts @@ -28,7 +28,12 @@ export default createTestingLibraryRule({ docs: { description: 'Disallow the use of `cleanup`', category: 'Best Practices', - recommended: false, + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { noManualCleanup: diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index 52de053f..5827298b 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -13,7 +13,12 @@ export default createTestingLibraryRule({ docs: { description: 'Disallow direct Node access', category: 'Best Practices', - recommended: 'error', + recommendedConfig: { + dom: false, + angular: 'error', + react: 'error', + vue: 'error', + }, }, messages: { noNodeAccess: diff --git a/lib/rules/no-promise-in-fire-event.ts b/lib/rules/no-promise-in-fire-event.ts index 4a765e6b..ac731719 100644 --- a/lib/rules/no-promise-in-fire-event.ts +++ b/lib/rules/no-promise-in-fire-event.ts @@ -20,7 +20,12 @@ export default createTestingLibraryRule({ description: 'Disallow the use of promises passed to a `fireEvent` method', category: 'Best Practices', - recommended: false, + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + }, }, messages: { noPromiseInFireEvent: diff --git a/lib/rules/no-render-in-setup.ts b/lib/rules/no-render-in-setup.ts index 37f96f5b..c445e53e 100644 --- a/lib/rules/no-render-in-setup.ts +++ b/lib/rules/no-render-in-setup.ts @@ -50,7 +50,12 @@ export default createTestingLibraryRule({ description: 'Disallow the use of `render` in testing frameworks setup functions', category: 'Best Practices', - recommended: false, + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { noRenderInSetup: diff --git a/lib/rules/no-wait-for-empty-callback.ts b/lib/rules/no-wait-for-empty-callback.ts index 04a622a4..97e2ac64 100644 --- a/lib/rules/no-wait-for-empty-callback.ts +++ b/lib/rules/no-wait-for-empty-callback.ts @@ -18,7 +18,12 @@ export default createTestingLibraryRule({ description: "It's preferred to avoid empty callbacks in `waitFor` and `waitForElementToBeRemoved`", category: 'Best Practices', - recommended: 'error', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + }, }, messages: { noWaitForEmptyCallback: diff --git a/lib/rules/no-wait-for-multiple-assertions.ts b/lib/rules/no-wait-for-multiple-assertions.ts index aff2f943..e540c428 100644 --- a/lib/rules/no-wait-for-multiple-assertions.ts +++ b/lib/rules/no-wait-for-multiple-assertions.ts @@ -16,7 +16,12 @@ export default createTestingLibraryRule({ docs: { description: "It's preferred to avoid multiple assertions in `waitFor`", category: 'Best Practices', - recommended: false, + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { noWaitForMultipleAssertion: diff --git a/lib/rules/no-wait-for-side-effects.ts b/lib/rules/no-wait-for-side-effects.ts index ed7b2cb2..c48735ed 100644 --- a/lib/rules/no-wait-for-side-effects.ts +++ b/lib/rules/no-wait-for-side-effects.ts @@ -16,7 +16,12 @@ export default createTestingLibraryRule({ docs: { description: "It's preferred to avoid side effects in `waitFor`", category: 'Best Practices', - recommended: false, + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { noSideEffectsWaitFor: diff --git a/lib/rules/no-wait-for-snapshot.ts b/lib/rules/no-wait-for-snapshot.ts index 7637748a..2da611c4 100644 --- a/lib/rules/no-wait-for-snapshot.ts +++ b/lib/rules/no-wait-for-snapshot.ts @@ -19,7 +19,12 @@ export default createTestingLibraryRule({ description: 'Ensures no snapshot is generated inside of a `waitFor` call', category: 'Best Practices', - recommended: 'warn', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { noWaitForSnapshot: diff --git a/lib/rules/prefer-explicit-assert.ts b/lib/rules/prefer-explicit-assert.ts index 209c9a88..f70d3cfb 100644 --- a/lib/rules/prefer-explicit-assert.ts +++ b/lib/rules/prefer-explicit-assert.ts @@ -25,7 +25,12 @@ export default createTestingLibraryRule({ description: 'Suggest using explicit assertions rather than just `getBy*` queries', category: 'Best Practices', - recommended: false, + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { preferExplicitAssert: diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 93d71dc0..5d3b6f5d 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -56,7 +56,12 @@ export default createTestingLibraryRule({ description: 'Suggest using `find*` query instead of `waitFor` + `get*` to wait for elements', category: 'Best Practices', - recommended: 'warn', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + }, }, messages: { preferFindBy: diff --git a/lib/rules/prefer-presence-queries.ts b/lib/rules/prefer-presence-queries.ts index 97d4d329..a343b085 100644 --- a/lib/rules/prefer-presence-queries.ts +++ b/lib/rules/prefer-presence-queries.ts @@ -13,7 +13,12 @@ export default createTestingLibraryRule({ category: 'Best Practices', description: 'Ensure appropriate get*/query* queries are used with their respective matchers', - recommended: 'error', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { wrongPresenceQuery: diff --git a/lib/rules/prefer-screen-queries.ts b/lib/rules/prefer-screen-queries.ts index b8158bb7..f51b1aa1 100644 --- a/lib/rules/prefer-screen-queries.ts +++ b/lib/rules/prefer-screen-queries.ts @@ -37,7 +37,12 @@ export default createTestingLibraryRule({ docs: { description: 'Suggest using screen while querying', category: 'Best Practices', - recommended: 'error', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + }, }, messages: { preferScreenQueries: diff --git a/lib/rules/prefer-user-event.ts b/lib/rules/prefer-user-event.ts index 73bdb6c8..a836c8e9 100644 --- a/lib/rules/prefer-user-event.ts +++ b/lib/rules/prefer-user-event.ts @@ -66,7 +66,12 @@ export default createTestingLibraryRule({ docs: { description: 'Suggest using userEvent over fireEvent', category: 'Best Practices', - recommended: 'warn', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { preferUserEvent: diff --git a/lib/rules/prefer-wait-for.ts b/lib/rules/prefer-wait-for.ts index 8fe9c37c..08f7d6ed 100644 --- a/lib/rules/prefer-wait-for.ts +++ b/lib/rules/prefer-wait-for.ts @@ -26,7 +26,12 @@ export default createTestingLibraryRule({ docs: { description: 'Use `waitFor` instead of deprecated wait methods', category: 'Best Practices', - recommended: false, + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { preferWaitForMethod: diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index 90dbbc20..df636a9c 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -24,7 +24,12 @@ export default createTestingLibraryRule({ docs: { description: 'Enforce a valid naming for return value from `render`', category: 'Best Practices', - recommended: false, + recommendedConfig: { + dom: false, + angular: 'error', + react: 'error', + vue: 'error', + }, }, messages: { renderResultNamingConvention: `\`{{ renderResultName }}\` is not a recommended name for \`render\` returned value. Instead, you should destructure it, or name it using one of: ${ALLOWED_VAR_NAMES_TEXT}`, diff --git a/lib/utils/file-import.ts b/lib/utils/file-import.ts new file mode 100644 index 00000000..9dc9d1db --- /dev/null +++ b/lib/utils/file-import.ts @@ -0,0 +1,8 @@ +// Copied from https://github.com/babel/babel/blob/b35c78f08dd854b08575fc66ebca323fdbc59dab/packages/babel-helpers/src/helpers.js#L615-L619 +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const interopRequireDefault = (obj: any): { default: T } => + obj?.__esModule ? obj : { default: obj }; + +export const importDefault = (moduleName: string): T => + // eslint-disable-next-line @typescript-eslint/no-var-requires + interopRequireDefault(require(moduleName)).default; diff --git a/lib/utils.ts b/lib/utils/index.ts similarity index 98% rename from lib/utils.ts rename to lib/utils/index.ts index 71850468..106d5f36 100644 --- a/lib/utils.ts +++ b/lib/utils/index.ts @@ -1,3 +1,6 @@ +export * from './file-import'; +export * from './types'; + const combineQueries = (variants: string[], methods: string[]): string[] => { const combinedQueries: string[] = []; variants.forEach((variant) => { diff --git a/lib/utils/types.ts b/lib/utils/types.ts new file mode 100644 index 00000000..eeb3b507 --- /dev/null +++ b/lib/utils/types.ts @@ -0,0 +1,36 @@ +import type { TSESLint } from '@typescript-eslint/experimental-utils'; + +type RecommendedConfig = + | TSESLint.RuleMetaDataDocs['recommended'] + | [TSESLint.RuleMetaDataDocs['recommended'], ...TOptions]; + +// These 2 types are copied from @typescript-eslint/experimental-utils' CreateRuleMeta +// and modified to our needs +type TestingLibraryRuleMetaDocs = Omit< + TSESLint.RuleMetaDataDocs, + 'recommended' | 'url' +> & { + /** + * The recommendation level for the rule on a framework basis. + * Used by the build tools to generate the framework config. + * Set to false to not include it the config + */ + recommendedConfig: Record< + SupportedTestingFramework, + RecommendedConfig + >; +}; +export type TestingLibraryRuleMeta< + TMessageIds extends string, + TOptions extends readonly unknown[] +> = { + docs: TestingLibraryRuleMetaDocs; +} & Omit, 'docs'>; + +export const SUPPORTED_TESTING_FRAMEWORKS = [ + 'dom', + 'angular', + 'react', + 'vue', +] as const; +export type SupportedTestingFramework = typeof SUPPORTED_TESTING_FRAMEWORKS[number]; diff --git a/package.json b/package.json index b01517ff..2de1d9a2 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "postbuild": "cpy README.md ./dist && cpy package.json ./dist && cpy LICENSE ./dist", "format": "prettier --write .", "format:check": "prettier --check .", + "generate:configs": "ts-node tools/generate-configs", "lint": "eslint . --max-warnings 0 --ext .js,.ts", "lint:fix": "npm run lint -- --fix", "test": "jest", diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index d92023c0..d154465d 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should export configs that refer to actual rules 1`] = ` +exports[`should have run 'generate:configs' script when changing config rules 1`] = ` Object { "angular": Object { "plugins": Array [ diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index e76cb61e..5dacdb9e 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -26,7 +26,12 @@ export default createTestingLibraryRule({ docs: { description: 'Fake rule to test rule maker and detection helpers', category: 'Possible Errors', - recommended: false, + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + }, }, messages: { fakeError: 'fake error reported', diff --git a/tests/index.test.ts b/tests/index.test.ts index b89f9df9..2f9808cb 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,8 +1,11 @@ +import { exec } from 'child_process'; import { existsSync } from 'fs'; import { resolve } from 'path'; import plugin from '../lib'; +const generateConfigs = () => exec(`npm run generate:configs`); + const numberOfRules = 24; const ruleNames = Object.keys(plugin.rules); @@ -43,10 +46,16 @@ it('should have the correct amount of rules', () => { } }); +it("should have run 'generate:configs' script when changing config rules", async () => { + await generateConfigs(); + + const allConfigs = plugin.configs; + expect(allConfigs).toMatchSnapshot(); +}); + it('should export configs that refer to actual rules', () => { const allConfigs = plugin.configs; - expect(allConfigs).toMatchSnapshot(); expect(Object.keys(allConfigs)).toEqual(['dom', 'angular', 'react', 'vue']); const allConfigRules = Object.values(allConfigs) .map((config) => Object.keys(config.rules)) diff --git a/tools/generate-configs/index.ts b/tools/generate-configs/index.ts new file mode 100644 index 00000000..f688d567 --- /dev/null +++ b/tools/generate-configs/index.ts @@ -0,0 +1,36 @@ +import type { LinterConfigRules } from '../../lib/configs'; +import rules from '../../lib/rules'; +import { + SUPPORTED_TESTING_FRAMEWORKS, + SupportedTestingFramework, +} from '../../lib/utils'; + +import { LinterConfig, writeConfig } from './utils'; + +const RULE_NAME_PREFIX = 'testing-library/'; + +const getRecommendedRulesForTestingFramework = ( + framework: SupportedTestingFramework +): LinterConfigRules => + Object.entries(rules) + .filter(([_, { meta: { docs } }]) => + Boolean(docs.recommendedConfig[framework]) + ) + .reduce((allRules, [ruleName, { meta }]) => { + const name = `${RULE_NAME_PREFIX}${ruleName}`; + const recommendation = meta.docs.recommendedConfig[framework]; + + return { + ...allRules, + [name]: recommendation, + }; + }, {}); + +SUPPORTED_TESTING_FRAMEWORKS.forEach((framework) => { + const specificFrameworkConfig: LinterConfig = { + plugins: ['testing-library'], + rules: getRecommendedRulesForTestingFramework(framework), + }; + + writeConfig(specificFrameworkConfig, framework); +}); diff --git a/tools/generate-configs/utils.ts b/tools/generate-configs/utils.ts new file mode 100644 index 00000000..0394c044 --- /dev/null +++ b/tools/generate-configs/utils.ts @@ -0,0 +1,33 @@ +import { writeFileSync } from 'fs'; +import { resolve } from 'path'; + +import type { TSESLint } from '@typescript-eslint/experimental-utils'; +import { format, resolveConfig } from 'prettier'; + +const prettierConfig = resolveConfig.sync(__dirname); + +export type LinterConfig = TSESLint.Linter.Config; + +const addAutoGeneratedComment = (code: string) => + [ + '// THIS CODE WAS AUTOMATICALLY GENERATED', + '// DO NOT EDIT THIS CODE BY HAND', + '// YOU CAN REGENERATE IT USING npm run generate:configs', + '', + code, + ].join('\n'); + +/** + * Helper function writes configuration. + */ +export const writeConfig = (config: LinterConfig, configName: string): void => { + // note: we use `export =` because ESLint will import these configs via a commonjs import + const code = `export = ${JSON.stringify(config)};`; + const configStr = format(addAutoGeneratedComment(code), { + parser: 'typescript', + ...prettierConfig, + }); + const filePath = resolve(__dirname, `../../lib/configs/${configName}.ts`); + + writeFileSync(filePath, configStr); +};