diff --git a/.all-contributorsrc b/.all-contributorsrc index 5faed601..3998e5d6 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -21,7 +21,8 @@ "review", "test", "infra", - "bug" + "bug", + "maintenance" ] }, { @@ -412,7 +413,8 @@ "contributions": [ "code", "doc", - "test" + "test", + "maintenance" ] }, { @@ -454,8 +456,300 @@ "contributions": [ "bug" ] + }, + { + "login": "AriPerkkio", + "name": "Ari PerkkiΓΆ", + "avatar_url": "https://avatars.githubusercontent.com/u/14806298?v=4", + "profile": "https://codepen.io/ariperkkio/", + "contributions": [ + "test" + ] + }, + { + "login": "diegocasmo", + "name": "Diego Castillo", + "avatar_url": "https://avatars.githubusercontent.com/u/4553097?v=4", + "profile": "https://diegocasmo.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "bpinto", + "name": "Bruno Pinto", + "avatar_url": "https://avatars.githubusercontent.com/u/526122?v=4", + "profile": "http://bpinto.github.com", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "themagickoala", + "name": "themagickoala", + "avatar_url": "https://avatars.githubusercontent.com/u/48416253?v=4", + "profile": "https://github.com/themagickoala", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "PrashantAshok", + "name": "Prashant Ashok", + "avatar_url": "https://avatars.githubusercontent.com/u/5200733?v=4", + "profile": "https://github.com/PrashantAshok", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "IvanAprea", + "name": "Ivan Aprea", + "avatar_url": "https://avatars.githubusercontent.com/u/54630721?v=4", + "profile": "https://github.com/IvanAprea", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "Semigradsky", + "name": "Dmitry Semigradsky", + "avatar_url": "https://avatars.githubusercontent.com/u/1198848?v=4", + "profile": "https://semigradsky.dev/", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "sjarva", + "name": "Senja", + "avatar_url": "https://avatars.githubusercontent.com/u/1133238?v=4", + "profile": "https://github.com/sjarva", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "brenocota-hotmart", + "name": "Breno Cota", + "avatar_url": "https://avatars.githubusercontent.com/u/106157862?v=4", + "profile": "https://dbrno.vercel.app", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "NickBolles", + "name": "Nick Bolles", + "avatar_url": "https://avatars.githubusercontent.com/u/7891759?v=4", + "profile": "https://nickbolles.com", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "bmish", + "name": "Bryan Mishkin", + "avatar_url": "https://avatars.githubusercontent.com/u/698306?v=4", + "profile": "http://www.linkedin.com/in/bmish", + "contributions": [ + "doc", + "tool" + ] + }, + { + "login": "theredspoon", + "name": "Nim G", + "avatar_url": "https://avatars.githubusercontent.com/u/20975696?v=4", + "profile": "https://github.com/theredspoon", + "contributions": [ + "doc" + ] + }, + { + "login": "patriscus", + "name": "Patrick Ahmetovic", + "avatar_url": "https://avatars.githubusercontent.com/u/23729362?v=4", + "profile": "https://github.com/patriscus", + "contributions": [ + "ideas", + "code", + "test" + ] + }, + { + "login": "CodingItWrong", + "name": "Josh Justice", + "avatar_url": "https://avatars.githubusercontent.com/u/15832198?v=4", + "profile": "https://codingitwrong.com", + "contributions": [ + "code", + "test", + "doc", + "ideas" + ] + }, + { + "login": "obsoke", + "name": "Dale Karp", + "avatar_url": "https://avatars.githubusercontent.com/u/389851?v=4", + "profile": "https://dale.io", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "nathanmmiller", + "name": "Nathan", + "avatar_url": "https://avatars.githubusercontent.com/u/37555055?v=4", + "profile": "https://github.com/nathanmmiller", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "justintoman", + "name": "justintoman", + "avatar_url": "https://avatars.githubusercontent.com/u/11649507?v=4", + "profile": "https://github.com/justintoman", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "adevick", + "name": "Anthony Devick", + "avatar_url": "https://avatars.githubusercontent.com/u/106642175?v=4", + "profile": "https://github.com/adevick", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "maisano", + "name": "Richard Maisano", + "avatar_url": "https://avatars.githubusercontent.com/u/689081?v=4", + "profile": "https://github.com/maisano", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "doochik", + "name": "Aleksei Androsov", + "avatar_url": "https://avatars.githubusercontent.com/u/31961?v=4", + "profile": "https://github.com/doochik", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "NicolasBonduel", + "name": "Nicolas Bonduel", + "avatar_url": "https://avatars.githubusercontent.com/u/6507454?v=4", + "profile": "https://github.com/NicolasBonduel", + "contributions": [ + "doc" + ] + }, + { + "login": "lesha1201", + "name": "Alexey Ryabov", + "avatar_url": "https://avatars.githubusercontent.com/u/10157660?v=4", + "profile": "https://aryabov.com", + "contributions": [ + "maintenance" + ] + }, + { + "login": "Chamion", + "name": "Jemi Salo", + "avatar_url": "https://avatars.githubusercontent.com/u/22522302?v=4", + "profile": "https://github.com/Chamion", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "nostrorom", + "name": "nostro", + "avatar_url": "https://avatars.githubusercontent.com/u/49858211?v=4", + "profile": "https://github.com/nostrorom", + "contributions": [ + "code" + ] + }, + { + "login": "danielrentz", + "name": "Daniel Rentz", + "avatar_url": "https://avatars.githubusercontent.com/u/5064304?v=4", + "profile": "https://github.com/danielrentz", + "contributions": [ + "doc" + ] + }, + { + "login": "StyleShit", + "name": "StyleShit", + "avatar_url": "https://avatars.githubusercontent.com/u/32631382?v=4", + "profile": "https://github.com/StyleShit", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "y-hsgw", + "name": "Yukihiro Hasegawa", + "avatar_url": "https://avatars.githubusercontent.com/u/49516827?v=4", + "profile": "https://github.com/y-hsgw", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "puglyfe", + "name": "Charley Pugmire", + "avatar_url": "https://avatars.githubusercontent.com/u/3228931?v=4", + "profile": "https://www.charleypugmire.me", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "andreww2012", + "name": "Andrew Kazakov", + "avatar_url": "https://avatars.githubusercontent.com/u/6554045?v=4", + "profile": "https://github.com/andreww2012", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, - "skipCi": true + "skipCi": false, + "commitType": "docs" } diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..cf73e8cd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = tab diff --git a/.eslint-doc-generatorrc.js b/.eslint-doc-generatorrc.js new file mode 100644 index 00000000..84262866 --- /dev/null +++ b/.eslint-doc-generatorrc.js @@ -0,0 +1,18 @@ +const prettier = require('prettier'); +const prettierConfig = require('./.prettierrc.js'); + +/** @type {import('eslint-doc-generator').GenerateOptions} */ +const config = { + ignoreConfig: [ + 'flat/angular', + 'flat/dom', + 'flat/marko', + 'flat/react', + 'flat/svelte', + 'flat/vue', + ], + postprocess: (content) => + prettier.format(content, { ...prettierConfig, parser: 'markdown' }), +}; + +module.exports = config; diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..93a919dc --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,79 @@ +module.exports = { + root: true, + env: { + es6: true, + node: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:import/recommended', + 'plugin:jest/recommended', + 'plugin:jest-formatting/recommended', + 'prettier', + ], + rules: { + // Base + 'max-lines-per-function': 'off', + + // Import + 'import/order': [ + 'warn', + { + groups: ['builtin', 'external', 'parent', 'sibling', 'index'], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: false, + }, + }, + ], + 'import/first': 'error', + 'import/no-empty-named-blocks': 'error', + 'import/no-extraneous-dependencies': 'error', + 'import/no-mutable-exports': 'error', + 'import/no-named-default': 'error', + 'import/no-relative-packages': 'warn', + }, + overrides: [ + { + // TypeScript + files: ['**/*.ts?(x)'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: __dirname, + }, + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', + 'plugin:import/typescript', + ], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_' }, + ], + '@typescript-eslint/no-use-before-define': 'off', + + // Import + // Rules enabled by `import/recommended` but are better handled by + // TypeScript and @typescript-eslint. + 'import/default': 'off', + 'import/export': 'off', + 'import/namespace': 'off', + 'import/no-unresolved': 'off', + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.ts'], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + }, + ], +}; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 1a7ed2d4..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "env": { - "commonjs": true, - "es6": true, - "node": true, - "jest/globals": true - }, - "extends": [ - "kentcdodds", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier", - "plugin:jest/recommended", - "plugin:jest-formatting/recommended" - ], - "plugins": ["@typescript-eslint", "jest", "jest-formatting"], - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.eslint.json" - }, - "rules": { - // TS - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-unused-vars": [ - "warn", - { "argsIgnorePattern": "^_" } - ], - "@typescript-eslint/no-use-before-define": "off", - - // ESLint - "max-lines-per-function": "off", - "no-restricted-imports": [ - "error", - { - "patterns": ["@typescript-eslint/experimental-utils/dist/*"] - } - ], - - // Import - "import/no-import-module-exports": "off", - "import/order": [ - "warn", - { - "groups": ["builtin", "external", "parent", "sibling", "index"], - "newlines-between": "always", - "alphabetize": { - "order": "asc", - "caseInsensitive": false - } - } - ] - } -} diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 06db2a24..597afbf1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,19 @@ name: Bug Report description: File a bug report -labels: [bug] +labels: ['bug', 'triage'] +type: bug body: + - type: dropdown + id: read_troubleshooting + attributes: + label: Have you read the Troubleshooting section? + description: Please confirm you have read our Troubleshooting section before reporting a new bug + options: + - 'Yes' + - 'No' + validations: + required: true + - type: input id: plugin_version attributes: @@ -29,24 +41,6 @@ body: validations: required: true - - type: input - id: npm_yarn_version - attributes: - label: npm/yarn version - description: Tell us whether you use npm or yarn as your package manager, and what version. - placeholder: npm 6.14.13 - validations: - required: true - - - type: input - id: operating_system - attributes: - label: Operating system - description: Tell us what operating system you use, and what version. - placeholder: macOS Big Sur, version 11.4 - validations: - required: true - - type: textarea id: bug_description attributes: @@ -60,7 +54,7 @@ body: id: steps_to_reproduce attributes: label: Steps to reproduce - description: Give us a ordered list of the steps to reproduce the problem. + description: Give us an ordered list of the steps to reproduce the problem. placeholder: | 1. Go to ... 2. Do .... @@ -81,7 +75,7 @@ body: id: eslint_config attributes: label: ESLint configuration - description: Copy/paste your ESLint configuration into this field. + description: Copy/paste your ESLint configuration relevant for this plugin into this field. placeholder: 'Tip: you can find your ESLint configuration in the `.eslintrc` file.' validations: required: true @@ -90,7 +84,7 @@ body: id: rule_affected attributes: label: Rule(s) affected - description: Tell us what `eslint-pluging-testing-library` rule(s) are affected by this bug. + description: Tell us what `eslint-plugin-testing-library` rule(s) are affected by this bug. placeholder: 'Tip: check your `.eslintrc` for rules.' validations: required: true diff --git a/.github/ISSUE_TEMPLATE/propose_new_rule.yml b/.github/ISSUE_TEMPLATE/propose_new_rule.yml index 95a0ee15..7c15b31c 100644 --- a/.github/ISSUE_TEMPLATE/propose_new_rule.yml +++ b/.github/ISSUE_TEMPLATE/propose_new_rule.yml @@ -1,13 +1,14 @@ name: Propose a new rule description: Propose a new rule for the eslint-plugin-testing-library. -labels: [new rule] +labels: ['new rule', 'triage'] +type: feature body: - type: input id: name_for_new_rule attributes: label: Name for new rule description: Suggest a name for the new rule that follows the [rule naming conventions](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/CONTRIBUTING.md#rule-naming-conventions). - placeholder: prefer-wait-for + placeholder: prefer-find-by validations: required: true diff --git a/.github/ISSUE_TEMPLATE/request_general_change.yml b/.github/ISSUE_TEMPLATE/request_general_change.yml index 3cf1b3e4..b6d0e8fd 100644 --- a/.github/ISSUE_TEMPLATE/request_general_change.yml +++ b/.github/ISSUE_TEMPLATE/request_general_change.yml @@ -1,6 +1,7 @@ name: Request a general change description: Request a general change for the eslint-plugin-testing-library. -labels: [enhancement] +labels: ['enhancement', 'triage'] +type: task body: - type: input id: plugin_version diff --git a/.github/ISSUE_TEMPLATE/request_rule_change.yml b/.github/ISSUE_TEMPLATE/request_rule_change.yml index 81081560..8a0d06ea 100644 --- a/.github/ISSUE_TEMPLATE/request_rule_change.yml +++ b/.github/ISSUE_TEMPLATE/request_rule_change.yml @@ -1,6 +1,7 @@ name: Request a rule change description: Request a rule change for the eslint-plugin-testing-library. -labels: [enhancement] +labels: ['enhancement', 'triage'] +type: feature body: - type: input id: what_rule_do_you_want_to_change diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9abd3898..7010f1b7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,8 +1,6 @@ ## Checks - [ ] I have read the [contributing guidelines](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/CONTRIBUTING.md). -- [ ] If some rule is added/updated/removed, I've regenerated the rules list. -- [ ] If some rule meta info is changed, I've regenerated the plugin shared configs. ## Changes diff --git a/.github/stale.yml b/.github/stale.yml index c9714211..a292650c 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -2,11 +2,17 @@ daysUntilStale: 60 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 +# Only issues or pull requests with all of these labels are checked if stale +onlyLabels: + - 'awaiting response' + - 'new rule' + - enhancement + - invalid # Issues with these labels will never be considered stale exemptLabels: - pinned - security - - bug + - triage # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..970bb07c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + pull_request: + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + verifications: + name: Verifications + uses: ./.github/workflows/verifications.yml + + required-checks: + name: Require CI status checks + runs-on: ubuntu-latest + if: ${{ !cancelled() && github.event.action != 'closed' }} + needs: [verifications] + steps: + - run: ${{ !contains(needs.*.result, 'failure') }} + - run: ${{ !contains(needs.*.result, 'cancelled') }} diff --git a/.github/workflows/main-coverage.yml b/.github/workflows/main-coverage.yml new file mode 100644 index 00000000..5d50392d --- /dev/null +++ b/.github/workflows/main-coverage.yml @@ -0,0 +1,38 @@ +name: Code Coverage (main) +on: + push: + branches: + - 'main' + +permissions: + contents: read + statuses: write + +jobs: + coverage: + name: Code Coverage + runs-on: ubuntu-latest + timeout-minutes: 3 + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + cache: 'pnpm' + node-version-file: '.nvmrc' + + - name: Install dependencies + run: pnpm install + + - name: Run tests with coverage + run: pnpm run test:ci + + - name: Upload coverage report + uses: codecov/codecov-action@v5 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml deleted file mode 100644 index a1e12a94..00000000 --- a/.github/workflows/pipeline.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Pipeline - -on: - push: - branches: - # semantic-release valid branches, excluding all-contributors - - '+([0-9])?(.{+([0-9]),x}).x' - - 'main' - - 'next' - - 'next-major' - - 'beta' - - 'alpha' - - '!all-contributors/**' - pull_request: - types: [opened, synchronize] - -jobs: - code_validation: - name: Code Validation - runs-on: ubuntu-latest - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.1 - - - name: Checkout - uses: actions/checkout@v2 - - - name: Use Node - uses: actions/setup-node@v2 - with: - node-version: 16 - - - name: Install dependencies - uses: bahmutov/npm-install@v1 - with: - useLockFile: false - - - name: Check Types - run: npm run type-check - - - name: Lint code - run: npm run lint - - - name: Check format - run: npm run format:check - - tests: - name: Tests (Node v${{ matrix.node }} - ESLint v${{ matrix.eslint }}) - runs-on: ubuntu-latest - - strategy: - matrix: - node: [12.22.0, 12, 14.17.0, 14, '16.0', 16] - eslint: [7.5, 7, 8] - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.1 - - - name: Checkout - uses: actions/checkout@v2 - - - name: Use Node - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node }} - - - name: Install dependencies - uses: bahmutov/npm-install@v1 - with: - useLockFile: false - - - name: Install ESLint v${{ matrix.eslint }} - # force installation for now until we get ESLint and all plugins updated - # in dev dependencies - run: npm install --no-save --force eslint@${{ matrix.eslint }} - - - name: Run tests - run: npm run test:ci - - release: - name: NPM Release - needs: [code_validation, tests] - runs-on: ubuntu-latest - if: - ${{ github.repository == 'testing-library/eslint-plugin-testing-library' && - contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', - github.ref) && github.event_name == 'push' }} - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.1 - - - name: Checkout - uses: actions/checkout@v2 - - - name: Use Node - uses: actions/setup-node@v2 - with: - node-version: 16 - - - name: Install dependencies - uses: bahmutov/npm-install@v1 - with: - useLockFile: false - - - name: Build package - run: npm run build - - - name: Release new version to NPM - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npx semantic-release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..50df9564 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +name: Release + +on: + push: + branches: + # semantic-release valid branches + - '+([0-9])?(.{+([0-9]),x}).x' + - 'main' + - 'next' + - 'next-major' + - 'beta' + - 'alpha' + +concurrency: + group: release + cancel-in-progress: false + +permissions: + contents: write # to be able to publish a GitHub release + id-token: write # to enable use of OIDC for npm provenance + issues: write # to be able to comment on released issues + pull-requests: write # to be able to comment on released pull requests + +jobs: + publish: + name: Publish package + runs-on: ubuntu-latest + # Avoid publishing in forks + if: github.repository == 'testing-library/eslint-plugin-testing-library' + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + cache: 'pnpm' + node-version-file: '.nvmrc' + + - name: Install dependencies + run: pnpm install + + - name: Build package + run: pnpm run build + + - name: Release new version + run: pnpm exec semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_CONFIG_PROVENANCE: true + NPM_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }} diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml new file mode 100644 index 00000000..69ac4477 --- /dev/null +++ b/.github/workflows/smoke-test.yml @@ -0,0 +1,38 @@ +name: Smoke test + +on: + schedule: + - cron: '0 0 * * SUN' + workflow_dispatch: + release: + types: [published] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Use Node + uses: actions/setup-node@v4 + with: + cache: 'pnpm' + node-version-file: '.nvmrc' + + - run: | + pnpm install + pnpm run build + + - run: pnpm link + working-directory: ./dist + + - run: pnpm link eslint-plugin-testing-library + + - uses: AriPerkkio/eslint-remote-tester-run-action@v4 + with: + issue-title: 'Results of weekly scheduled smoke test' + eslint-remote-tester-config: tests/eslint-remote-tester.config.js diff --git a/.github/workflows/verifications.yml b/.github/workflows/verifications.yml new file mode 100644 index 00000000..27b58a0c --- /dev/null +++ b/.github/workflows/verifications.yml @@ -0,0 +1,68 @@ +name: Verifications + +on: + workflow_call: + +jobs: + code-validation: + name: 'Code Validation: ${{ matrix.validation-script }}' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + validation-script: + ['lint', 'type-check', 'format:check', 'generate-all:check'] + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + cache: 'pnpm' + node-version-file: '.nvmrc' + + - name: Install dependencies + run: pnpm install + + - name: Run script + run: pnpm run ${{ matrix.validation-script }} + + tests: + name: Tests (Node v${{ matrix.node }} - ESLint v${{ matrix.eslint }}) + runs-on: ubuntu-latest + timeout-minutes: 3 + strategy: + fail-fast: false + matrix: + node: [18.18.0, 18, 20.9.0, 20, 21.1.0, 21, 22, 23] + eslint: [8.57.0, 8, 9] + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + cache: 'pnpm' + node-version: ${{ matrix.node }} + + - name: Install dependencies + run: pnpm install + + - name: Install ESLint v${{ matrix.eslint }} + run: pnpm add eslint@${{ matrix.eslint }} + + - name: Run tests + run: pnpm run test:ci + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index e5dd00b0..776666b4 100644 --- a/.gitignore +++ b/.gitignore @@ -67,7 +67,6 @@ yarn-error.log # Yarn Integrity file .yarn-integrity -# these cause more harm than good -# when working with contributors +# Ignore locks other than pnpm package-lock.json yarn.lock diff --git a/.husky/commit-msg b/.husky/commit-msg index f137fefd..5254b5b1 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,7 +1,4 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - # Skip commit-msg hook on CI [ -n "$CI" ] && exit 0 -npx commitlint --edit $1 +commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit index 1500ab63..b4299251 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,7 +1,4 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - # Skip pre-commit hook on CI [ -n "$CI" ] && exit 0 -npx lint-staged +lint-staged diff --git a/.lintstagedrc b/.lintstagedrc deleted file mode 100644 index eff1e342..00000000 --- a/.lintstagedrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "*.{js,ts}": [ - "eslint --max-warnings 0 --fix", - "prettier --write", - "jest --findRelatedTests" - ], - "*.{json,md,yml}": ["prettier --write"] -} diff --git a/.npmrc b/.npmrc index 43c97e71..14c0beb1 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ -package-lock=false +auto-install-peers=true +enable-pre-post-scripts=true +public-hoist-pattern[]=@commitlint* +public-hoist-pattern[]=commitlint \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..54abcf34 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist +node_modules +coverage +.all-contributorsrc +pnpm-lock.yaml \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js index e340799c..d82995ee 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,3 +1,5 @@ module.exports = { - singleQuote: true, + trailingComma: 'es5', + singleQuote: true, + useTabs: true, }; diff --git a/.releaserc.json b/.releaserc.json index dd5a8466..485cf943 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -1,17 +1,16 @@ { - "pkgRoot": "dist", - "branches": [ - "+([0-9])?(.{+([0-9]),x}).x", - "main", - "next", - "next-major", - { - "name": "beta", - "prerelease": true - }, - { - "name": "alpha", - "prerelease": true - } - ] + "branches": [ + "+([0-9])?(.{+([0-9]),x}).x", + "main", + "next", + "next-major", + { + "name": "beta", + "prerelease": true + }, + { + "name": "alpha", + "prerelease": true + } + ] } diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ca41f9e2..54fe7651 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,75 +2,131 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -- The use of sexualized language or imagery and unwelcome sexual attention or - advances -- Trolling, insulting/derogatory comments, and personal or political attacks +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission +- Publishing others' private information, such as a physical or email address, + without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at belco90@gmail.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at +belco90+coc@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -[homepage]: https://www.contributor-covenant.org +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][mozilla coc]. -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d92df51..1818459a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ Thanks for being willing to contribute! Working on your first Pull Request? You can learn how from this free series -[How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). +[How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). Tweaking ESLint rules is mostly about traversing through the AST. [AST Explorer](https://astexplorer.net) is a great tool that simplifies the process. @@ -11,7 +11,7 @@ Tweaking ESLint rules is mostly about traversing through the AST. [AST Explorer] 1. Fork this repository 2. Clone your forked repository -3. Run `npm install` to install corresponding dependencies +3. Run `pnpm install` to install corresponding dependencies 4. Create a branch for your PR named like `pr/your-branch-name` (you can do this through git CLI with `git checkout -b pr/your-branch-name`) > Tip: Keep your `main` branch pointing at the original repository and make @@ -38,8 +38,6 @@ The following will be run on every commit: - Check all tests are passing - Check commit message is following [Conventional Commit specification](https://www.conventionalcommits.org/en/v1.0.0/) -If you ever need to update a snapshot, you can run `npm run test:update` - ## Rule naming conventions Based on [ESLint's Rule Naming Conventions](https://eslint.org/docs/developer-guide/working-with-rules#rule-naming-conventions), you must follow these rules: @@ -65,7 +63,7 @@ each rule has three files named with its identifier (e.g. `no-debugging-utils`): Additionally, you need to do a couple of extra things: -- Run `npm run generate:rules-list` to include your rule in the "Supported Rules" table within the [README.md](./README.md) +- Run `pnpm run generate:rules-doc` to include your rule in the "Supported Rules" table within the [README.md](./README.md) ### Custom rule creator @@ -101,7 +99,7 @@ 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. +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 (`pnpm run generate:configs`) our configs. ## Updating existing rules @@ -118,10 +116,10 @@ If you wish to run a single test while developing locally, add `only: true` to t ```javascript valid: [ - { - only: true, - code: `...`, - }, + { + only: true, + code: `...`, + }, ]; ``` @@ -137,9 +135,9 @@ Since the plugin will report differently depending on which Testing Library pack import { render } from '@testing-library/react'; test('should report invalid render usage', () => { - // the following line is the actual code you needed to test your rule, - // but everything else helps finding edge cases and makes it more robust. - const wrapper = render(); + // the following line is the actual code you needed to test your rule, + // but everything else helps finding edge cases and makes it more robust. + const wrapper = render(); }); ``` diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 00000000..1d92022d --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,52 @@ +This document outlines some processes that the maintainers should stick to. + +## Node.js version + +We will try to stick to the same range of [Node.js versions supported by ESLint](https://github.com/eslint/eslint#installation-and-usage) as much as possible. + +For local development and CI, we will use the Maintenance LTS version (specified in `.nvmrc` file). + +## Issues Process + +There are 3 types of issues that can be created: + +- `"bug"` +- `"new rule"` +- `"enhancement"` + +### Triage + +The triage process is basically making sure that the issue is correctly reported (and ask for more info if not), and categorize it correctly if it belongs to a different type than initially assigned. + +- When a new issue is created, they'll include a `"triage"` label +- If the issue is correctly reported, please remove the `"triage"` label, so we know is valid and ready to be tackled +- If the issue is **not** correctly reported, please ask for more details and add the `"awaiting response"` label, so we know more info has been requested to the author +- If the issue belong to an incorrect category, please update the labels to put it in the right category +- If the issue is duplicated, please close it including a comment with a link to the duplicating issue + +## Pull Requests Process + +### Main PR workflow + +_TODO: pending to describe the main PR process_ + +### Contributors + +When the PR gets merged, please check if the author of the PR or the closed issue (if any) should be added or updated in the [Contributors section](https://github.com/testing-library/eslint-plugin-testing-library#contributors-). + +If so, you can ask the [`@all-contributors` bot to add a contributor](https://allcontributors.org/docs/en/bot/usage) in a comment of the merged PR (this works for both adding and updating). Remember to check the [Contribution Types table](https://allcontributors.org/docs/en/emoji-key) to decide which sort of contribution should be assigned. + +## Stale bot + +This repo uses [probot-stale](https://github.com/probot/stale) to close abandoned issues and PRs after a period of inactivity. + +They'll be considered inactive if they match all the following conditions: + +- they have been 60 days inactive +- they have at least one of the following labels: + - `"awaiting response"`: we are waiting for more details but the author didn't react + - `"new rule"`: there is a proposal for a new rule that no one could handle + - `"enhancement"`: there is a proposal for an enhancement that no one could handle + - `"invalid"`: something is wrong with the issue/PR and the author didn't take care of it + +When flagged as a stale issue or PR, they'll be definitely closed after 7 more days of inactivity. Issues and PRs with the following labels are excluded: `"pinned"`, `"security"`, and "`triage"`. Use the first one if you need to exclude an issue or PR from being closed for whatever reason even if the inactive criteria is matched. diff --git a/README.md b/README.md index c6cba0dc..5c6649ae 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,40 @@
- + +

eslint-plugin-testing-library

ESLint plugin to follow best practices and anticipate common mistakes when writing tests with Testing Library

--- -[![Build status][build-badge]][build-url] [![Package version][version-badge]][version-url] [![eslint-remote-tester][eslint-remote-tester-badge]][eslint-remote-tester-workflow] [![eslint-plugin-testing-library][package-health-badge]][package-health-url] +[![codecov](https://codecov.io/gh/testing-library/eslint-plugin-testing-library/graph/badge.svg?token=IJd6ZogYPm)](https://codecov.io/gh/testing-library/eslint-plugin-testing-library) [![MIT License][license-badge]][license-url]
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release) [![PRs Welcome][pr-badge]][pr-url] [![All Contributors][all-contributors-badge]](#contributors-) -
-[![Watch on Github][gh-watchers-badge]][gh-watchers-url] -[![Star on Github][gh-stars-badge]][gh-stars-url] -[![Tweet][tweet-badge]][tweet-url] -## Installation +## Prerequisites -You'll first need to install [ESLint](https://eslint.org): +To use this plugin, you must have [Node.js](https://nodejs.org/en/) (`^18.18.0`, `^20.9.0`, or `>=21.1.0`) installed. -```shell -$ npm install --save-dev eslint -# or -$ yarn add --dev eslint -``` +## Installation + +You'll first need to [install ESLint](https://eslint.org/docs/latest/use/getting-started). Next, install `eslint-plugin-testing-library`: ```shell +$ pnpm add --save-dev eslint-plugin-testing-library +# or $ npm install --save-dev eslint-plugin-testing-library # or $ yarn add --dev eslint-plugin-testing-library @@ -49,30 +46,32 @@ $ yarn add --dev eslint-plugin-testing-library You can find detailed guides for migrating `eslint-plugin-testing-library` in the [migration guide docs](docs/migration-guides): -- [Migrate guide for v4](docs/migration-guides/v4.md) -- [Migrate guide for v5](docs/migration-guides/v5.md) +- [Migration guide for v4](docs/migration-guides/v4.md) +- [Migration guide for v5](docs/migration-guides/v5.md) +- [Migration guide for v6](docs/migration-guides/v6.md) +- [Migration guide for v7](docs/migration-guides/v7.md) ## Usage -Add `testing-library` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: +Add `testing-library` to the plugins section of your `.eslintrc.js` configuration file. You can omit the `eslint-plugin-` prefix: -```json -{ - "plugins": ["testing-library"] -} +```js +module.exports = { + plugins: ['testing-library'], +}; ``` Then configure the rules you want to use within `rules` property of your `.eslintrc`: -```json -{ - "rules": { - "testing-library/await-async-query": "error", - "testing-library/no-await-sync-query": "error", - "testing-library/no-debugging-utils": "warn", - "testing-library/no-dom-import": "off" - } -} +```js +module.exports = { + rules: { + 'testing-library/await-async-queries': 'error', + 'testing-library/no-await-sync-queries': 'error', + 'testing-library/no-debugging-utils': 'warn', + 'testing-library/no-dom-import': 'off', + }, +}; ``` ### Run the plugin only against test files @@ -85,22 +84,22 @@ One way of restricting ESLint config by file patterns is by using [ESLint `overr Assuming you are using the same pattern for your test files as [Jest by default](https://jestjs.io/docs/configuration#testmatch-arraystring), the following config would run `eslint-plugin-testing-library` only against your test files: -```javascript -// .eslintrc -{ - // 1) Here we have our usual config which applies to the whole project, so we don't put testing-library preset here. - "extends": ["airbnb", "plugin:prettier/recommended"], - - // 2) We load eslint-plugin-testing-library globally with other ESLint plugins. - "plugins": ["react-hooks", "testing-library"], - - "overrides": [ - { - // 3) Now we enable eslint-plugin-testing-library rules or preset only for matching files! - "files": ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"], - "extends": ["plugin:testing-library/react"] - }, - ], +```js +// .eslintrc.js +module.exports = { + // 1) Here we have our usual config which applies to the whole project, so we don't put testing-library preset here. + extends: ['airbnb', 'plugin:prettier/recommended'], + + // 2) We load other plugins than eslint-plugin-testing-library globally if we want to. + plugins: ['react-hooks'], + + overrides: [ + { + // 3) Now we enable eslint-plugin-testing-library rules or preset only for matching testing files! + files: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], + extends: ['plugin:testing-library/react'], + }, + ], }; ``` @@ -110,21 +109,33 @@ Another approach for customizing ESLint config by paths is through [ESLint Casca ## Shareable configurations +> [!NOTE] +> +> `eslint.config.js` compatible versions of configs are available prefixed with +> `flat/`, though most of the plugin documentation still currently uses +> `.eslintrc` syntax. +> +> Refer to the +> [ESLint documentation on the new configuration file format](https://eslint.org/docs/latest/use/configure/configuration-files-new) +> for more. + This plugin exports several recommended configurations that enforce good practices for specific Testing Library packages. You can find more info about enabled rules in the [Supported Rules section](#supported-rules), under the `Configurations` column. Since each one of these configurations is aimed at a particular Testing Library package, they are not extendable between them, so you should use only one of them at once per `.eslintrc` file. For example, if you want to enable recommended configuration for React, you don't need to combine it somehow with DOM one: -```json +```js // ❌ Don't do this -{ - "extends": ["plugin:testing-library/dom", "plugin:testing-library/react"] -} - -// βœ… Do just this instead -{ - "extends": ["plugin:testing-library/react"] -} +module.exports = { + extends: ['plugin:testing-library/dom', 'plugin:testing-library/react'], +}; +``` + +```js +// βœ… Just do this instead +module.exports = { + extends: ['plugin:testing-library/react'], +}; ``` ### DOM Testing Library @@ -132,12 +143,28 @@ Since each one of these configurations is aimed at a particular Testing Library Enforces recommended rules for DOM Testing Library. To enable this configuration use the `extends` property in your -`.eslintrc` config file: +`.eslintrc.js` config file: -```json -{ - "extends": ["plugin:testing-library/dom"] -} +```js +module.exports = { + extends: ['plugin:testing-library/dom'], +}; +``` + +To enable this configuration with `eslint.config.js`, use +`testingLibrary.configs['flat/dom']`: + +```js +const testingLibrary = require('eslint-plugin-testing-library'); + +module.exports = [ + { + files: [ + /* glob matching your test files */ + ], + ...testingLibrary.configs['flat/dom'], + }, +]; ``` ### Angular @@ -145,12 +172,28 @@ To enable this configuration use the `extends` property in your Enforces recommended rules for Angular Testing Library. To enable this configuration use the `extends` property in your -`.eslintrc` config file: +`.eslintrc.js` config file: -```json -{ - "extends": ["plugin:testing-library/angular"] -} +```js +module.exports = { + extends: ['plugin:testing-library/angular'], +}; +``` + +To enable this configuration with `eslint.config.js`, use +`testingLibrary.configs['flat/angular']`: + +```js +const testingLibrary = require('eslint-plugin-testing-library'); + +module.exports = [ + { + files: [ + /* glob matching your test files */ + ], + ...testingLibrary.configs['flat/angular'], + }, +]; ``` ### React @@ -158,12 +201,28 @@ To enable this configuration use the `extends` property in your Enforces recommended rules for React Testing Library. To enable this configuration use the `extends` property in your -`.eslintrc` config file: +`.eslintrc.js` config file: + +```js +module.exports = { + extends: ['plugin:testing-library/react'], +}; +``` + +To enable this configuration with `eslint.config.js`, use +`testingLibrary.configs['flat/react']`: + +```js +const testingLibrary = require('eslint-plugin-testing-library'); -```json -{ - "extends": ["plugin:testing-library/react"] -} +module.exports = [ + { + files: [ + /* glob matching your test files */ + ], + ...testingLibrary.configs['flat/react'], + }, +]; ``` ### Vue @@ -171,52 +230,130 @@ To enable this configuration use the `extends` property in your Enforces recommended rules for Vue Testing Library. To enable this configuration use the `extends` property in your -`.eslintrc` config file: +`.eslintrc.js` config file: -```json -{ - "extends": ["plugin:testing-library/vue"] -} +```js +module.exports = { + extends: ['plugin:testing-library/vue'], +}; +``` + +To enable this configuration with `eslint.config.js`, use +`testingLibrary.configs['flat/vue']`: + +```js +const testingLibrary = require('eslint-plugin-testing-library'); + +module.exports = [ + { + files: [ + /* glob matching your test files */ + ], + ...testingLibrary.configs['flat/vue'], + }, +]; +``` + +### Svelte + +Enforces recommended rules for Svelte Testing Library. + +To enable this configuration use the `extends` property in your +`.eslintrc.js` config file: + +```js +module.exports = { + extends: ['plugin:testing-library/svelte'], +}; +``` + +To enable this configuration with `eslint.config.js`, use +`testingLibrary.configs['flat/svelte']`: + +```js +const testingLibrary = require('eslint-plugin-testing-library'); + +module.exports = [ + { + files: [ + /* glob matching your test files */ + ], + ...testingLibrary.configs['flat/svelte'], + }, +]; +``` + +### Marko + +Enforces recommended rules for Marko Testing Library. + +To enable this configuration use the `extends` property in your +`.eslintrc.js` config file: + +```js +module.exports = { + extends: ['plugin:testing-library/marko'], +}; +``` + +To enable this configuration with `eslint.config.js`, use +`testingLibrary.configs['flat/marko']`: + +```js +const testingLibrary = require('eslint-plugin-testing-library'); + +module.exports = [ + { + files: [ + /* glob matching your test files */ + ], + ...testingLibrary.configs['flat/marko'], + }, +]; ``` ## Supported Rules - - -**Key**: πŸ”§ = fixable - -**Configurations**: ![dom-badge][] = dom, ![angular-badge][] = angular, ![react-badge][] = react, ![vue-badge][] = vue - -| Name | Description | πŸ”§ | Included in configurations | -| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | --- | ----------------------------------------------------------------- | -| [`testing-library/await-async-query`](./docs/rules/await-async-query.md) | Enforce promises from async queries to be handled | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/await-async-utils`](./docs/rules/await-async-utils.md) | Enforce promises from async utils to be awaited properly | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/await-fire-event`](./docs/rules/await-fire-event.md) | Enforce promises from `fireEvent` methods to be handled | | ![vue-badge][] | -| [`testing-library/consistent-data-testid`](./docs/rules/consistent-data-testid.md) | Ensures consistent usage of `data-testid` | | | -| [`testing-library/no-await-sync-events`](./docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | | | -| [`testing-library/no-await-sync-query`](./docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/no-container`](./docs/rules/no-container.md) | Disallow the use of `container` methods | | ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/no-debugging-utils`](./docs/rules/no-debugging-utils.md) | Disallow the use of debugging utilities like `debug` | | ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/no-dom-import`](./docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | πŸ”§ | ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/no-manual-cleanup`](./docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | -| [`testing-library/no-node-access`](./docs/rules/no-node-access.md) | Disallow direct Node access | | ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/no-promise-in-fire-event`](./docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/no-render-in-setup`](./docs/rules/no-render-in-setup.md) | Disallow the use of `render` in testing frameworks setup functions | | ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/no-unnecessary-act`](./docs/rules/no-unnecessary-act.md) | Disallow wrapping Testing Library utils or empty callbacks in `act` | | ![react-badge][] | -| [`testing-library/no-wait-for-empty-callback`](./docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/no-wait-for-multiple-assertions`](./docs/rules/no-wait-for-multiple-assertions.md) | Disallow the use of multiple `expect` calls inside `waitFor` | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/no-wait-for-side-effects`](./docs/rules/no-wait-for-side-effects.md) | Disallow the use of side effects in `waitFor` | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/no-wait-for-snapshot`](./docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/prefer-explicit-assert`](./docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than standalone queries | | | -| [`testing-library/prefer-find-by`](./docs/rules/prefer-find-by.md) | Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements | πŸ”§ | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/prefer-presence-queries`](./docs/rules/prefer-presence-queries.md) | Ensure appropriate `get*`/`query*` queries are used with their respective matchers | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/prefer-query-by-disappearance`](./docs/rules/prefer-query-by-disappearance.md) | Suggest using `queryBy*` queries when waiting for disappearance | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/prefer-screen-queries`](./docs/rules/prefer-screen-queries.md) | Suggest using `screen` while querying | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | -| [`testing-library/prefer-user-event`](./docs/rules/prefer-user-event.md) | Suggest using `userEvent` over `fireEvent` for simulating user interactions | | | -| [`testing-library/prefer-wait-for`](./docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | πŸ”§ | | -| [`testing-library/render-result-naming-convention`](./docs/rules/render-result-naming-convention.md) | Enforce a valid naming for return value from `render` | | ![angular-badge][] ![react-badge][] ![vue-badge][] | - - +> Remember that all rules from this plugin are prefixed by `"testing-library/"` + + + +πŸ’Ό Configurations enabled in.\ +⚠️ Configurations set to warn in.\ +πŸ”§ Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). + +| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β  | Description | πŸ’Ό | ⚠️ | πŸ”§ | +| :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------ | :-- | +| [await-async-events](docs/rules/await-async-events.md) | Enforce promises from async event methods are handled | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | πŸ”§ | +| [await-async-queries](docs/rules/await-async-queries.md) | Enforce promises from async queries to be handled | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [await-async-utils](docs/rules/await-async-utils.md) | Enforce promises from async utils to be awaited properly | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensures consistent usage of `data-testid` | | | | +| [no-await-sync-events](docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | ![badge-angular][] ![badge-dom][] ![badge-react][] | | | +| [no-await-sync-queries](docs/rules/no-await-sync-queries.md) | Disallow unnecessary `await` for sync queries | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![badge-angular][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [no-debugging-utils](docs/rules/no-debugging-utils.md) | Disallow the use of debugging utilities like `debug` | | ![badge-angular][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | +| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![badge-angular][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | πŸ”§ | +| [no-global-regexp-flag-in-query](docs/rules/no-global-regexp-flag-in-query.md) | Disallow the use of the global RegExp flag (/g) in queries | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | πŸ”§ | +| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [no-render-in-lifecycle](docs/rules/no-render-in-lifecycle.md) | Disallow the use of `render` in testing frameworks setup functions | ![badge-angular][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [no-test-id-queries](docs/rules/no-test-id-queries.md) | Ensure no `data-testid` queries are used | | | | +| [no-unnecessary-act](docs/rules/no-unnecessary-act.md) | Disallow wrapping Testing Library utils or empty callbacks in `act` | ![badge-marko][] ![badge-react][] | | | +| [no-wait-for-multiple-assertions](docs/rules/no-wait-for-multiple-assertions.md) | Disallow the use of multiple `expect` calls inside `waitFor` | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [no-wait-for-side-effects](docs/rules/no-wait-for-side-effects.md) | Disallow the use of side effects in `waitFor` | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than standalone queries | | | | +| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | πŸ”§ | +| [prefer-implicit-assert](docs/rules/prefer-implicit-assert.md) | Suggest using implicit assertions for getBy* & findBy* queries | | | | +| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Ensure appropriate `get*`/`query*` queries are used with their respective matchers | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | πŸ”§ | +| [prefer-query-by-disappearance](docs/rules/prefer-query-by-disappearance.md) | Suggest using `queryBy*` queries when waiting for disappearance | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [prefer-query-matchers](docs/rules/prefer-query-matchers.md) | Ensure the configured `get*`/`query*` query is used with the corresponding matchers | | | | +| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using `screen` while querying | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [prefer-user-event](docs/rules/prefer-user-event.md) | Suggest using `userEvent` over `fireEvent` for simulating user interactions | | | | +| [render-result-naming-convention](docs/rules/render-result-naming-convention.md) | Enforce a valid naming for return value from `render` | ![badge-angular][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | + + ## Aggressive Reporting @@ -234,13 +371,13 @@ If you are sure about configuring the settings, these are the options available: The name of your custom utility file from where you re-export everything from the Testing Library package, or `"off"` to switch related Aggressive Reporting mechanism off. Relates to [Aggressive Imports Reporting](docs/migration-guides/v4.md#imports). -```json -// .eslintrc -{ - "settings": { - "testing-library/utils-module": "my-custom-test-utility-file" - } -} +```js +// .eslintrc.js +module.exports = { + settings: { + 'testing-library/utils-module': 'my-custom-test-utility-file', + }, +}; ``` [You can find more details about the `utils-module` setting here](docs/migration-guides/v4.md#testing-libraryutils-module). @@ -249,13 +386,13 @@ The name of your custom utility file from where you re-export everything from th A list of function names that are valid as Testing Library custom renders, or `"off"` to switch related Aggressive Reporting mechanism off. Relates to [Aggressive Renders Reporting](docs/migration-guides/v4.md#renders). -```json -// .eslintrc -{ - "settings": { - "testing-library/custom-renders": ["display", "renderWithProviders"] - } -} +```js +// .eslintrc.js +module.exports = { + settings: { + 'testing-library/custom-renders': ['display', 'renderWithProviders'], + }, +}; ``` [You can find more details about the `custom-renders` setting here](docs/migration-guides/v4.md#testing-librarycustom-renders). @@ -264,13 +401,13 @@ A list of function names that are valid as Testing Library custom renders, or `" A list of query names/patterns that are valid as Testing Library custom queries, or `"off"` to switch related Aggressive Reporting mechanism off. Relates to [Aggressive Reporting - Queries](docs/migration-guides/v4.md#queries) -```json -// .eslintrc -{ - "settings": { - "testing-library/custom-queries": ["ByIcon", "getByComplexText"] - } -} +```js +// .eslintrc.js +module.exports = { + settings: { + 'testing-library/custom-queries': ['ByIcon', 'getByComplexText'], + }, +}; ``` [You can find more details about the `custom-queries` setting here](docs/migration-guides/v4.md#testing-librarycustom-queries). @@ -279,15 +416,15 @@ A list of query names/patterns that are valid as Testing Library custom queries, Since each Shared Setting is related to one Aggressive Reporting mechanism, and they accept `"off"` to opt out of that mechanism, you can switch the entire feature off by doing: -```json -// .eslintrc -{ - "settings": { - "testing-library/utils-module": "off", - "testing-library/custom-renders": "off", - "testing-library/custom-queries": "off" - } -} +```js +// .eslintrc.js +module.exports = { + settings: { + 'testing-library/utils-module': 'off', + 'testing-library/custom-renders': 'off', + 'testing-library/custom-queries': 'off', + }, +}; ``` ## Troubleshooting @@ -327,60 +464,101 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Mario BeltrΓ‘n AlarcΓ³n

πŸ’» πŸ“– πŸ‘€ ⚠️ πŸš‡ πŸ›

Thomas Lombart

πŸ’» πŸ“– πŸ‘€ ⚠️ πŸš‡

Ben Monro

πŸ’» πŸ“– ⚠️

Nicola Molinari

πŸ’» ⚠️ πŸ“– πŸ‘€

AarΓ³n GarcΓ­a HervΓ‘s

πŸ“–

Matej Ε nuderl

πŸ€” πŸ“–

AdriΓ  Fontcuberta

πŸ’» ⚠️

Jon Aldinger

πŸ“–

Thomas Knickman

πŸ’» πŸ“– ⚠️

Kevin Sullivan

πŸ“–

Jakub JastrzΔ™bski

πŸ’» πŸ“– ⚠️

Nikolay Stoynov

πŸ“–

marudor

πŸ’» ⚠️

Tim Deschryver

πŸ’» πŸ“– πŸ€” πŸ‘€ ⚠️ πŸ› πŸš‡ πŸ“¦

Tobias Deekens

πŸ›

Victor Cordova

πŸ’» ⚠️ πŸ›

Dmitry Lobanov

πŸ’» ⚠️

Kent C. Dodds

πŸ›

Gonzalo D'Elia

πŸ’» ⚠️ πŸ“– πŸ‘€

Jeff Rifwald

πŸ“–

Leandro Lourenci

πŸ› πŸ’» ⚠️

Miguel Erja GonzΓ‘lez

πŸ›

Pavel Pustovalov

πŸ›

Jacob Parish

πŸ› πŸ’» ⚠️

Nick McCurdy

πŸ€” πŸ’» πŸ‘€

Stefan Cameron

πŸ›

Mateus Felix

πŸ’» ⚠️ πŸ“–

Renato Augusto Gama dos Santos

πŸ€” πŸ’» πŸ“– ⚠️

Josh Kelly

πŸ’»

Alessia Bellisario

πŸ’» ⚠️ πŸ“–

Spencer Miskoviak

πŸ’» ⚠️ πŸ“– πŸ€”

Giorgio Polvara

πŸ’» ⚠️ πŸ“–

Josh David

πŸ“–

MichaΓ«l De Boey

πŸ’» πŸ“¦ 🚧 πŸš‡ πŸ‘€

Jian Huang

πŸ’» ⚠️ πŸ“–

Philipp Fritsche

πŸ’»

Tomas Zaicevas

πŸ› πŸ’» ⚠️ πŸ“–

Gareth Jones

πŸ’» πŸ“– ⚠️

HonkingGoose

πŸ“– 🚧

Julien Wajsberg

πŸ› πŸ’» ⚠️

Marat Dyatko

πŸ› πŸ’»

David Tolman

πŸ›
Mario BeltrΓ‘n AlarcΓ³n
Mario BeltrΓ‘n AlarcΓ³n

πŸ’» πŸ“– πŸ‘€ ⚠️ πŸš‡ πŸ› 🚧
Thomas Lombart
Thomas Lombart

πŸ’» πŸ“– πŸ‘€ ⚠️ πŸš‡
Ben Monro
Ben Monro

πŸ’» πŸ“– ⚠️
Nicola Molinari
Nicola Molinari

πŸ’» ⚠️ πŸ“– πŸ‘€
AarΓ³n GarcΓ­a HervΓ‘s
AarΓ³n GarcΓ­a HervΓ‘s

πŸ“–
Matej Ε nuderl
Matej Ε nuderl

πŸ€” πŸ“–
AdriΓ  Fontcuberta
AdriΓ  Fontcuberta

πŸ’» ⚠️
Jon Aldinger
Jon Aldinger

πŸ“–
Thomas Knickman
Thomas Knickman

πŸ’» πŸ“– ⚠️
Kevin Sullivan
Kevin Sullivan

πŸ“–
Jakub JastrzΔ™bski
Jakub JastrzΔ™bski

πŸ’» πŸ“– ⚠️
Nikolay Stoynov
Nikolay Stoynov

πŸ“–
marudor
marudor

πŸ’» ⚠️
Tim Deschryver
Tim Deschryver

πŸ’» πŸ“– πŸ€” πŸ‘€ ⚠️ πŸ› πŸš‡ πŸ“¦
Tobias Deekens
Tobias Deekens

πŸ›
Victor Cordova
Victor Cordova

πŸ’» ⚠️ πŸ›
Dmitry Lobanov
Dmitry Lobanov

πŸ’» ⚠️
Kent C. Dodds
Kent C. Dodds

πŸ›
Gonzalo D'Elia
Gonzalo D'Elia

πŸ’» ⚠️ πŸ“– πŸ‘€
Jeff Rifwald
Jeff Rifwald

πŸ“–
Leandro Lourenci
Leandro Lourenci

πŸ› πŸ’» ⚠️
Miguel Erja GonzΓ‘lez
Miguel Erja GonzΓ‘lez

πŸ›
Pavel Pustovalov
Pavel Pustovalov

πŸ›
Jacob Parish
Jacob Parish

πŸ› πŸ’» ⚠️
Nick McCurdy
Nick McCurdy

πŸ€” πŸ’» πŸ‘€
Stefan Cameron
Stefan Cameron

πŸ›
Mateus Felix
Mateus Felix

πŸ’» ⚠️ πŸ“–
Renato Augusto Gama dos Santos
Renato Augusto Gama dos Santos

πŸ€” πŸ’» πŸ“– ⚠️
Josh Kelly
Josh Kelly

πŸ’»
Alessia Bellisario
Alessia Bellisario

πŸ’» ⚠️ πŸ“–
Spencer Miskoviak
Spencer Miskoviak

πŸ’» ⚠️ πŸ“– πŸ€”
Giorgio Polvara
Giorgio Polvara

πŸ’» ⚠️ πŸ“–
Josh David
Josh David

πŸ“–
MichaΓ«l De Boey
MichaΓ«l De Boey

πŸ’» πŸ“¦ 🚧 πŸš‡ πŸ‘€
Jian Huang
Jian Huang

πŸ’» ⚠️ πŸ“–
Philipp Fritsche
Philipp Fritsche

πŸ’»
Tomas Zaicevas
Tomas Zaicevas

πŸ› πŸ’» ⚠️ πŸ“–
Gareth Jones
Gareth Jones

πŸ’» πŸ“– ⚠️ 🚧
HonkingGoose
HonkingGoose

πŸ“– 🚧
Julien Wajsberg
Julien Wajsberg

πŸ› πŸ’» ⚠️
Marat Dyatko
Marat Dyatko

πŸ› πŸ’»
David Tolman
David Tolman

πŸ›
Ari PerkkiΓΆ
Ari PerkkiΓΆ

⚠️
Diego Castillo
Diego Castillo

πŸ’»
Bruno Pinto
Bruno Pinto

πŸ’» ⚠️
themagickoala
themagickoala

πŸ’» ⚠️
Prashant Ashok
Prashant Ashok

πŸ’» ⚠️
Ivan Aprea
Ivan Aprea

πŸ’» ⚠️
Dmitry Semigradsky
Dmitry Semigradsky

πŸ’» ⚠️ πŸ“–
Senja
Senja

πŸ’» ⚠️ πŸ“–
Breno Cota
Breno Cota

πŸ’» ⚠️
Nick Bolles
Nick Bolles

πŸ’» ⚠️ πŸ“–
Bryan Mishkin
Bryan Mishkin

πŸ“– πŸ”§
Nim G
Nim G

πŸ“–
Patrick Ahmetovic
Patrick Ahmetovic

πŸ€” πŸ’» ⚠️
Josh Justice
Josh Justice

πŸ’» ⚠️ πŸ“– πŸ€”
Dale Karp
Dale Karp

πŸ’» ⚠️ πŸ“–
Nathan
Nathan

πŸ’» ⚠️
justintoman
justintoman

πŸ’» ⚠️
Anthony Devick
Anthony Devick

πŸ’» ⚠️ πŸ“–
Richard Maisano
Richard Maisano

πŸ’» ⚠️
Aleksei Androsov
Aleksei Androsov

πŸ’» ⚠️
Nicolas Bonduel
Nicolas Bonduel

πŸ“–
Alexey Ryabov
Alexey Ryabov

🚧
Jemi Salo
Jemi Salo

πŸ’» ⚠️
nostro
nostro

πŸ’»
Daniel Rentz
Daniel Rentz

πŸ“–
StyleShit
StyleShit

πŸ’» ⚠️ πŸ“–
Yukihiro Hasegawa
Yukihiro Hasegawa

πŸ’» ⚠️
Charley Pugmire
Charley Pugmire

πŸ’» ⚠️
Andrew Kazakov
Andrew Kazakov

πŸ’»
@@ -390,26 +568,20 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! -[build-badge]: https://github.com/testing-library/eslint-plugin-testing-library/actions/workflows/pipeline.yml/badge.svg -[build-url]: https://github.com/testing-library/eslint-plugin-testing-library/actions/workflows/pipeline.yml [version-badge]: https://img.shields.io/npm/v/eslint-plugin-testing-library [version-url]: https://www.npmjs.com/package/eslint-plugin-testing-library [license-badge]: https://img.shields.io/npm/l/eslint-plugin-testing-library -[eslint-remote-tester-badge]: https://img.shields.io/github/workflow/status/AriPerkkio/eslint-remote-tester/eslint-plugin-testing-library?label=eslint-remote-tester -[eslint-remote-tester-workflow]: https://github.com/AriPerkkio/eslint-remote-tester/actions?query=workflow%3Aeslint-plugin-testing-library +[license-url]: https://github.com/testing-library/eslint-plugin-testing-library/blob/main/LICENSE +[eslint-remote-tester-badge]: https://img.shields.io/github/actions/workflow/status/AriPerkkio/eslint-remote-tester/lint-eslint-plugin-testing-library.yml +[eslint-remote-tester-workflow]: https://github.com/AriPerkkio/eslint-remote-tester/actions/workflows/lint-eslint-plugin-testing-library.yml [package-health-badge]: https://snyk.io/advisor/npm-package/eslint-plugin-testing-library/badge.svg [package-health-url]: https://snyk.io/advisor/npm-package/eslint-plugin-testing-library -[license-url]: https://github.com/testing-library/eslint-plugin-testing-library/blob/main/license [pr-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square [all-contributors-badge]: https://img.shields.io/github/all-contributors/testing-library/eslint-plugin-testing-library?color=orange&style=flat-square [pr-url]: http://makeapullrequest.com -[gh-watchers-badge]: https://img.shields.io/github/watchers/testing-library/eslint-plugin-testing-library?style=social -[gh-watchers-url]: https://github.com/testing-library/eslint-plugin-testing-library/watchers -[gh-stars-badge]: https://img.shields.io/github/stars/testing-library/eslint-plugin-testing-library?style=social -[gh-stars-url]: https://github.com/testing-library/eslint-plugin-testing-library/stargazers -[tweet-badge]: https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Ftesting-library%2Feslint-plugin-testing-library -[tweet-url]: https://twitter.com/intent/tweet?url=https%3a%2f%2fgithub.com%2ftesting-library%2feslint-plugin-testing-library&text=check%20out%20eslint-plugin-testing-library%20by%20@belcodev -[dom-badge]: https://img.shields.io/badge/%F0%9F%90%99-DOM-black?style=flat-square -[angular-badge]: https://img.shields.io/badge/-Angular-black?style=flat-square&logo=angular&logoColor=white&labelColor=DD0031&color=black -[react-badge]: https://img.shields.io/badge/-React-black?style=flat-square&logo=react&logoColor=white&labelColor=61DAFB&color=black -[vue-badge]: https://img.shields.io/badge/-Vue-black?style=flat-square&logo=vue.js&logoColor=white&labelColor=4FC08D&color=black +[badge-dom]: https://img.shields.io/badge/%F0%9F%90%99-DOM-black?style=flat-square +[badge-angular]: https://img.shields.io/badge/-Angular-black?style=flat-square&logo=angular&logoColor=white&labelColor=DD0031&color=black +[badge-react]: https://img.shields.io/badge/-React-black?style=flat-square&logo=react&logoColor=white&labelColor=61DAFB&color=black +[badge-svelte]: https://img.shields.io/badge/-Svelte-black?style=flat-square&logo=svelte&logoColor=white&labelColor=FF3E00&color=black +[badge-vue]: https://img.shields.io/badge/-Vue-black?style=flat-square&logo=vue.js&logoColor=white&labelColor=4FC08D&color=black +[badge-marko]: https://img.shields.io/badge/-Marko-black?style=flat-square&logo=marko&logoColor=white&labelColor=2596BE&color=black diff --git a/commitlint.config.js b/commitlint.config.js index 84dcb122..4c73b71e 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,3 +1,3 @@ module.exports = { - extends: ['@commitlint/config-conventional'], + extends: ['@commitlint/config-conventional'], }; diff --git a/docs/migration-guides/v4.md b/docs/migration-guides/v4.md index 4d89d6c8..d0966a88 100644 --- a/docs/migration-guides/v4.md +++ b/docs/migration-guides/v4.md @@ -182,13 +182,13 @@ Relates to the [Aggressive Imports Reporting mechanism](#imports). This setting If you pass a string other than `"off"` to this option, it will represent your custom utility file from where you re-export everything from Testing Library package. -```json -// .eslintrc -{ - "settings": { - "testing-library/utils-module": "my-custom-test-utility-file" - } -} +```js +// .eslintrc.js +module.exports = { + settings: { + 'testing-library/utils-module': 'my-custom-test-utility-file', + }, +}; ``` Configuring this setting like that, you'll restrict the errors reported by the plugin to only those utils being imported from this custom utility file, or some `@testing-library/*` package. The previous setting example would cause: @@ -197,9 +197,9 @@ Configuring this setting like that, you'll restrict the errors reported by the p import { waitFor } from '@testing-library/react'; test('testing-library/utils-module setting example', () => { - // βœ… this would be reported since this invalid usage of an util - // is imported from `@testing-library/*` package - waitFor(/* some invalid usage to be reported */); + // βœ… this would be reported since this invalid usage of an util + // is imported from `@testing-library/*` package + waitFor(/* some invalid usage to be reported */); }); ``` @@ -207,9 +207,9 @@ test('testing-library/utils-module setting example', () => { import { waitFor } from '../my-custom-test-utility-file'; test('testing-library/utils-module setting example', () => { - // βœ… this would be reported since this invalid usage of an util - // is imported from specified custom utility file. - waitFor(/* some invalid usage to be reported */); + // βœ… this would be reported since this invalid usage of an util + // is imported from specified custom utility file. + waitFor(/* some invalid usage to be reported */); }); ``` @@ -217,21 +217,21 @@ test('testing-library/utils-module setting example', () => { import { waitFor } from '../somewhere-else'; test('testing-library/utils-module setting example', () => { - // ❌ this would NOT be reported since this invalid usage of an util - // is NOT imported from either `@testing-library/*` package or specified custom utility file. - waitFor(/* some invalid usage to be reported */); + // ❌ this would NOT be reported since this invalid usage of an util + // is NOT imported from either `@testing-library/*` package or specified custom utility file. + waitFor(/* some invalid usage to be reported */); }); ``` You can also set this setting to `"off"` to entirely opt-out Aggressive Imports Reporting mechanism, so only utils coming from Testing Library packages are reported. -```json -// .eslintrc -{ - "settings": { - "testing-library/utils-module": "off" - } -} +```js +// .eslintrc.js +module.exports = { + settings: { + 'testing-library/utils-module': 'off', + }, +}; ``` ### `testing-library/custom-renders` @@ -240,23 +240,23 @@ Relates to the [Aggressive Renders Reporting mechanism](#renders). This setting If you pass an array of strings to this option, it will represent a list of function names that are valid as Testing Library custom renders. -```json -// .eslintrc -{ - "settings": { - "testing-library/custom-renders": ["display", "renderWithProviders"] - } -} +```js +// .eslintrc.js +module.exports = { + settings: { + 'testing-library/custom-renders': ['display', 'renderWithProviders'], + }, +}; ``` Configuring this setting like that, you'll restrict the errors reported by the plugin related to `render` somehow to only those functions sharing a name with one of the elements of that list, or built-in `render`. The previous setting example would cause: ```javascript import { - render, - display, - renderWithProviders, - renderWithRedux, + render, + display, + renderWithProviders, + renderWithRedux, } from 'test-utils'; import Component from 'somewhere'; @@ -264,37 +264,37 @@ const setupA = () => renderWithProviders(); const setupB = () => renderWithRedux(); test('testing-library/custom-renders setting example', () => { - // βœ… this would be reported since `render` is a built-in Testing Library util - const invalidUsage = render(); + // βœ… this would be reported since `render` is a built-in Testing Library util + const invalidUsage = render(); - // βœ… this would be reported since `display` has been set as `custom-render` - const invalidUsage = display(); + // βœ… this would be reported since `display` has been set as `custom-render` + const invalidUsage = display(); - // βœ… this would be reported since `renderWithProviders` has been set as `custom-render` - const invalidUsage = renderWithProviders(); + // βœ… this would be reported since `renderWithProviders` has been set as `custom-render` + const invalidUsage = renderWithProviders(); - // ❌ this would NOT be reported since `renderWithRedux` isn't a `custom-render` or built-in one - const invalidUsage = renderWithRedux(); + // ❌ this would NOT be reported since `renderWithRedux` isn't a `custom-render` or built-in one + const invalidUsage = renderWithRedux(); - // βœ… this would be reported since it wraps `renderWithProviders`, - // which has been set as `custom-render` - const invalidUsage = setupA(); + // βœ… this would be reported since it wraps `renderWithProviders`, + // which has been set as `custom-render` + const invalidUsage = setupA(); - // ❌ this would NOT be reported since it wraps `renderWithRedux`, - // which isn't a `custom-render` or built-in one - const invalidUsage = setupB(); + // ❌ this would NOT be reported since it wraps `renderWithRedux`, + // which isn't a `custom-render` or built-in one + const invalidUsage = setupB(); }); ``` You can also set this setting to `"off"` to entirely opt-out Aggressive Renders Reporting mechanism, so only methods named `render` are reported as Testing Library render util. -```json -// .eslintrc -{ - "settings": { - "testing-library/custom-renders": "off" - } -} +```js +// .eslintrc.js +module.exports = { + settings: { + 'testing-library/custom-renders': 'off', + }, +}; ``` ### `testing-library/custom-queries` @@ -308,13 +308,13 @@ Each string passed to this list of custom queries can be: - **pattern query (recommended)**: a custom query variant (suffix starting with "By") to be reported, so all query combinations around it are reported. For instance: `"ByIcon"` would report all `getByIcon()`, `getAllByIcon()`, `queryByIcon()` and `findByIcon()`. - **strict query**: a specific custom query name to be reported, so only that very exact query would be reported but not any related variant. For instance: `"getByIcon"` would make the plugin to report `getByIcon()` but not `getAllByIcon()`, `queryByIcon()` or `findByIcon()`. -```json -// .eslintrc -{ - "settings": { - "testing-library/custom-queries": ["ByIcon", "getByComplexText"] - } -} +```js +// .eslintrc.js +module.exports = { + settings: { + 'testing-library/custom-queries': ['ByIcon', 'getByComplexText'], + }, +}; ``` Configuring this setting like that, you'll restrict the errors reported by the plugin related to the queries to only those custom queries matching name or pattern from that list, or [built-in queries](https://testing-library.com/docs/queries/about). The previous setting example would cause: @@ -344,11 +344,11 @@ findBySomethingElse('foo'); You can also set this setting to `"off"` to entirely opt-out Aggressive Queries Reporting mechanism, so only built-in queries are reported. -```json -// .eslintrc -{ - "settings": { - "testing-library/custom-queries": "off" - } -} +```js +// .eslintrc.js +module.exports = { + settings: { + 'testing-library/custom-queries': 'off', + }, +}; ``` diff --git a/docs/migration-guides/v6.md b/docs/migration-guides/v6.md new file mode 100644 index 00000000..bc885a59 --- /dev/null +++ b/docs/migration-guides/v6.md @@ -0,0 +1,30 @@ +# Guide: migrating to v6 + +If you are not on v5 yet, we recommend first following the [v5 migration guide](docs/migration-guides/v5.md). + +## Overview + +- `prefer-wait-for` was removed +- `no-wait-for-empty-callback` was removed +- `await-fire-event` is now called `await-async-events` with support for an `eventModule` option with `userEvent` and/or `fireEvent` +- `await-async-events` is now enabled by default for `fireEvent` in Vue and Marko shared configs +- `await-async-events` is now enabled by default for `userEvent` in all shared configs +- `await-async-query` is now called `await-async-queries` +- `no-await-sync-query` is now called `no-await-sync-queries` +- `no-render-in-setup` is now called `no-render-in-lifecycle` +- `no-await-sync-events` is now enabled by default in React, Angular, and DOM shared configs +- `no-manual-cleanup` is now enabled by default in React and Vue shared configs +- `no-global-regexp-flag-in-query` is now enabled by default in all shared configs +- `no-node-access` is now enabled by default in DOM shared config +- `no-debugging-utils` now reports all debugging utility methods by default +- `no-debugging-utils` now defaults to `warn` instead of `error` in all shared configs + +## Steps to upgrade + +- Removing `testing-library/prefer-wait-for` if you were referencing it manually somewhere +- Removing `testing-library/no-wait-for-empty-callback` if you were referencing it manually somewhere +- Renaming `testing-library/await-fire-event` to `testing-library/await-async-events` if you were referencing it manually somewhere +- Renaming `testing-library/await-async-query` to `testing-library/await-async-queries` if you were referencing it manually somewhere +- Renaming `testing-library/no-await-sync-query` to `testing-library/no-await-sync-queries` if you were referencing it manually somewhere +- Renaming `testing-library/no-render-in-setup` to `testing-library/no-render-in-lifecycle` if you were referencing it manually somewhere +- Being aware of new rules enabled or changed above in shared configs which can lead to newly reported errors diff --git a/docs/migration-guides/v7.md b/docs/migration-guides/v7.md new file mode 100644 index 00000000..f46957a9 --- /dev/null +++ b/docs/migration-guides/v7.md @@ -0,0 +1,14 @@ +# Guide: migrating to v7 + +If you are not on v6 yet, we recommend first following the [v6 migration guide](docs/migration-guides/v6.md). + +## Overview + +- **(Breaking)** Supported versions of Node.js have been updated to `^18.18.0`, `^20.9.0`, or `>=21.1.0`, matching ESLint. +- **(Breaking)** Supported versions of ESLint have been updated to `^8.57.0`, or `^9.0.0`. +- Full support for ESLint v9 (v8 still compatible) and typescript-eslint v8 + +## Steps to upgrade + +1. Make sure you are using a supported version of Node.js, and upgrade if not. +2. Make sure you are using a supported version of ESLint, and upgrade if not. diff --git a/docs/rules/await-async-events.md b/docs/rules/await-async-events.md new file mode 100644 index 00000000..2adb43a1 --- /dev/null +++ b/docs/rules/await-async-events.md @@ -0,0 +1,149 @@ +# Enforce promises from async event methods are handled (`testing-library/await-async-events`) + +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + +πŸ”§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +Ensure that promises returned by `userEvent` (v14+) async methods or `fireEvent` (only Vue and Marko) async methods are handled properly. + +## Rule Details + +This rule aims to prevent users from forgetting to handle promise returned from async event +methods. + +> ⚠️ `fireEvent` methods are async only on following Testing Library packages: +> +> - `@testing-library/vue` (supported by this plugin) +> - `@testing-library/svelte` (not supported yet by this plugin) +> - `@marko/testing-library` (supported by this plugin) + +Examples of **incorrect** code for this rule: + +```js +fireEvent.click(getByText('Click me')); + +fireEvent.focus(getByLabelText('username')); +fireEvent.blur(getByLabelText('username')); + +// wrap a fireEvent method within a function... +function triggerEvent() { + return fireEvent.click(button); +} +triggerEvent(); // ...but not handling promise from it is incorrect too +``` + +```js +userEvent.click(getByText('Click me')); +userEvent.tripleClick(getByText('Click me')); +userEvent.keyboard('foo'); + +// wrap a userEvent method within a function... +function triggerEvent() { + return userEvent.click(button); +} +triggerEvent(); // ...but not handling promise from it is incorrect too +``` + +Examples of **correct** code for this rule: + +```js +// `await` operator is correct +await fireEvent.focus(getByLabelText('username')); +await fireEvent.blur(getByLabelText('username')); + +// `then` method is correct +fireEvent.click(getByText('Click me')).then(() => { + // ... +}); + +// return the promise within a function is correct too! +const clickMeArrowFn = () => fireEvent.click(getByText('Click me')); + +// wrap a fireEvent method within a function... +function triggerEvent() { + return fireEvent.click(button); +} +await triggerEvent(); // ...and handling promise from it is correct also + +// using `Promise.all` or `Promise.allSettled` with an array of promises is valid +await Promise.all([ + fireEvent.focus(getByLabelText('username')), + fireEvent.blur(getByLabelText('username')), +]); +``` + +```js +// `await` operator is correct +await userEvent.click(getByText('Click me')); +await userEvent.tripleClick(getByText('Click me')); + +// `then` method is correct +userEvent.keyboard('foo').then(() => { + // ... +}); + +// return the promise within a function is correct too! +const clickMeArrowFn = () => userEvent.click(getByText('Click me')); + +// wrap a userEvent method within a function... +function triggerEvent() { + return userEvent.click(button); +} +await triggerEvent(); // ...and handling promise from it is correct also + +// using `Promise.all` or `Promise.allSettled` with an array of promises is valid +await Promise.all([ + userEvent.click(getByText('Click me')); + userEvent.tripleClick(getByText('Click me')); +]); +``` + +## Options + +- `eventModule`: `string` or `string[]`. Which event module should be linted for async event methods. Defaults to `userEvent` which should be used after v14. `fireEvent` should only be used with frameworks that have async fire event methods. + +## Example + +```json +{ + "testing-library/await-async-events": [ + 2, + { + "eventModule": "userEvent" + } + ] +} +``` + +```json +{ + "testing-library/await-async-events": [ + 2, + { + "eventModule": "fireEvent" + } + ] +} +``` + +```json +{ + "testing-library/await-async-events": [ + 2, + { + "eventModule": ["fireEvent", "userEvent"] + } + ] +} +``` + +## When Not To Use It + +- `userEvent` is below v14, before all event methods are async +- `fireEvent` methods are sync for most Testing Library packages. If you are not using Testing Library package with async events, you shouldn't use this rule. + +## Further Reading + +- [Vue Testing Library fireEvent](https://testing-library.com/docs/vue-testing-library/api#fireevent) diff --git a/docs/rules/await-async-query.md b/docs/rules/await-async-queries.md similarity index 81% rename from docs/rules/await-async-query.md rename to docs/rules/await-async-queries.md index c0f8081f..be99be6a 100644 --- a/docs/rules/await-async-query.md +++ b/docs/rules/await-async-queries.md @@ -1,4 +1,8 @@ -# Enforce promises from async queries to be handled (`testing-library/await-async-query`) +# Enforce promises from async queries to be handled (`testing-library/await-async-queries`) + +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + + Ensure that promises returned by async queries are handled properly. @@ -19,6 +23,8 @@ problems in the tests. The promise will be considered as handled when: - wrapped within `Promise.all` or `Promise.allSettled` methods - chaining the `then` method - chaining `resolves` or `rejects` from jest +- chaining `toResolve()` or `toReject()` from [jest-extended](https://github.com/jest-community/jest-extended#promise) +- chaining jasmine [async matchers](https://jasmine.github.io/api/edge/async-matchers.html) - it's returned from a function (in this case, that particular function will be analyzed by this rule too) Examples of **incorrect** code for this rule: @@ -79,8 +85,8 @@ await Promise.all([findByText('my button'), findByText('something else')]); ```js // several promises handled `Promise.allSettled` is correct await Promise.allSettled([ - findByText('my button'), - findByText('something else'), + findByText('my button'), + findByText('something else'), ]); ``` @@ -90,6 +96,12 @@ expect(findByTestId('alert')).resolves.toBe('Success'); expect(findByTestId('alert')).rejects.toBe('Error'); ``` +```js +// using a toResolve/toReject matcher is also correct +expect(findByTestId('alert')).toResolve(); +expect(findByTestId('alert')).toReject(); +``` + ```js // sync queries don't need to handle any promise const element = getByRole('role'); diff --git a/docs/rules/await-async-utils.md b/docs/rules/await-async-utils.md index 9d23ab41..c42e047b 100644 --- a/docs/rules/await-async-utils.md +++ b/docs/rules/await-async-utils.md @@ -1,4 +1,8 @@ -# Enforce promises from async utils to be handled (`testing-library/await-async-utils`) +# Enforce promises from async utils to be awaited properly (`testing-library/await-async-utils`) + +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + + Ensure that promises returned by async utils are handled properly. @@ -6,11 +10,8 @@ Ensure that promises returned by async utils are handled properly. Testing library provides several utilities for dealing with asynchronous code. These are useful to wait for an element until certain criteria or situation happens. The available async utils are: -- `waitFor` _(introduced since dom-testing-library v7)_ +- `waitFor` - `waitForElementToBeRemoved` -- `wait` _(**deprecated** since dom-testing-library v7)_ -- `waitForElement` _(**deprecated** since dom-testing-library v7)_ -- `waitForDomChange` _(**deprecated** since dom-testing-library v7)_ This rule aims to prevent users from forgetting to handle the returned promise from async utils, which could lead to @@ -20,34 +21,36 @@ problems in the tests. The promise will be considered as handled when: - wrapped within `Promise.all` or `Promise.allSettled` methods - chaining the `then` method - chaining `resolves` or `rejects` from jest +- chaining `toResolve()` or `toReject()` from [jest-extended](https://github.com/jest-community/jest-extended#promise) +- chaining jasmine [async matchers](https://jasmine.github.io/api/edge/async-matchers.html) - it's returned from a function (in this case, that particular function will be analyzed by this rule too) Examples of **incorrect** code for this rule: ```js test('something incorrectly', async () => { - // ... - waitFor(() => {}); - - const [usernameElement, passwordElement] = waitFor( - () => [ - getByLabelText(container, 'username'), - getByLabelText(container, 'password'), - ], - { container } - ); - - waitFor(() => {}, { timeout: 100 }); - - waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere')); - - // wrap an async util within a function... - const makeCustomWait = () => { - return waitForElementToBeRemoved(() => - document.querySelector('div.getOuttaHere') - ); - }; - makeCustomWait(); // ...but not handling promise from it is incorrect + // ... + waitFor(() => {}); + + const [usernameElement, passwordElement] = waitFor( + () => [ + getByLabelText(container, 'username'), + getByLabelText(container, 'password'), + ], + { container } + ); + + waitFor(() => {}, { timeout: 100 }); + + waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere')); + + // wrap an async util within a function... + const makeCustomWait = () => { + return waitForElementToBeRemoved(() => + document.querySelector('div.getOuttaHere') + ); + }; + makeCustomWait(); // ...but not handling promise from it is incorrect }); ``` @@ -55,36 +58,42 @@ Examples of **correct** code for this rule: ```js test('something correctly', async () => { - // ... - // `await` operator is correct - await waitFor(() => getByLabelText('email')); - - const [usernameElement, passwordElement] = await waitFor( - () => [ - getByLabelText(container, 'username'), - getByLabelText(container, 'password'), - ], - { container } - ); - - // `then` chained method is correct - waitFor(() => {}, { timeout: 100 }) - .then(() => console.log('DOM changed!')) - .catch((err) => console.log(`Error you need to deal with: ${err}`)); - - // wrap an async util within a function... - const makeCustomWait = () => { - return waitForElementToBeRemoved(() => - document.querySelector('div.getOuttaHere') - ); - }; - await makeCustomWait(); // ...and handling promise from it is correct - - // using Promise.all combining the methods - await Promise.all([ - waitFor(() => getByLabelText('email')), - waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere')), - ]); + // ... + // `await` operator is correct + await waitFor(() => getByLabelText('email')); + + const [usernameElement, passwordElement] = await waitFor( + () => [ + getByLabelText(container, 'username'), + getByLabelText(container, 'password'), + ], + { container } + ); + + // `then` chained method is correct + waitFor(() => {}, { timeout: 100 }) + .then(() => console.log('DOM changed!')) + .catch((err) => console.log(`Error you need to deal with: ${err}`)); + + // wrap an async util within a function... + const makeCustomWait = () => { + return waitForElementToBeRemoved(() => + document.querySelector('div.getOuttaHere') + ); + }; + await makeCustomWait(); // ...and handling promise from it is correct + + // using Promise.all combining the methods + await Promise.all([ + waitFor(() => getByLabelText('email')), + waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere')), + ]); + + // Using jest resolves or rejects + expect(waitFor(() => getByLabelText('email'))).resolves.toBeUndefined(); + + // Using jest-extended a toResolve/toReject matcher is also correct + expect(waitFor(() => getByLabelText('email'))).toResolve(); }); ``` diff --git a/docs/rules/await-fire-event.md b/docs/rules/await-fire-event.md deleted file mode 100644 index 65e28594..00000000 --- a/docs/rules/await-fire-event.md +++ /dev/null @@ -1,65 +0,0 @@ -# Enforce promises from fire event methods to be handled (`testing-library/await-fire-event`) - -Ensure that promises returned by `fireEvent` methods are handled -properly. - -## Rule Details - -This rule aims to prevent users from forgetting to handle promise returned from `fireEvent` -methods. - -> ⚠️ `fireEvent` methods are async only on following Testing Library packages: -> -> - `@testing-library/vue` (supported by this plugin) -> - `@testing-library/svelte` (not supported yet by this plugin) - -Examples of **incorrect** code for this rule: - -```js -fireEvent.click(getByText('Click me')); - -fireEvent.focus(getByLabelText('username')); -fireEvent.blur(getByLabelText('username')); - -// wrap a fireEvent method within a function... -function triggerEvent() { - return fireEvent.click(button); -} -triggerEvent(); // ...but not handling promise from it is incorrect too -``` - -Examples of **correct** code for this rule: - -```js -// `await` operator is correct -await fireEvent.focus(getByLabelText('username')); -await fireEvent.blur(getByLabelText('username')); - -// `then` method is correct -fireEvent.click(getByText('Click me')).then(() => { - // ... -}); - -// return the promise within a function is correct too! -const clickMeArrowFn = () => fireEvent.click(getByText('Click me')); - -// wrap a fireEvent method within a function... -function triggerEvent() { - return fireEvent.click(button); -} -await triggerEvent(); // ...and handling promise from it is correct also - -// using `Promise.all` or `Promise.allSettled` with an array of promises is valid -await Promise.all([ - fireEvent.focus(getByLabelText('username')), - fireEvent.blur(getByLabelText('username')), -]); -``` - -## When Not To Use It - -`fireEvent` methods are not async on all Testing Library packages. If you are not using Testing Library package with async fire event, you shouldn't use this rule. - -## Further Reading - -- [Vue Testing Library fireEvent](https://testing-library.com/docs/vue-testing-library/api#fireevent) diff --git a/docs/rules/consistent-data-testid.md b/docs/rules/consistent-data-testid.md index 9b03f2ae..bcefc54e 100644 --- a/docs/rules/consistent-data-testid.md +++ b/docs/rules/consistent-data-testid.md @@ -1,7 +1,13 @@ -# Enforces consistent naming for the data-testid attribute (`testing-library/consistent-data-testid`) +# Ensures consistent usage of `data-testid` (`testing-library/consistent-data-testid`) + + Ensure `data-testid` values match a provided regex. This rule is un-opinionated, and requires configuration. +> ⚠️ This rule is only available in the following Testing Library packages: +> +> - `@testing-library/react` (supported by this plugin) + ## Rule Details > Assuming the rule has been configured with the following regex: `^TestId(\_\_[A-Z]*)?$` @@ -24,31 +30,47 @@ const baz = (props) =>
...
; ## Options -| Option | Required | Default | Details | Example | -| ----------------- | -------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `testIdPattern` | Yes | None | A regex used to validate the format of the `data-testid` value. `{fileName}` can optionally be used as a placeholder and will be substituted with the name of the file OR the name of the files parent directory in the case when the file name is `index.js` | `^{fileName}(\_\_([A-Z]+[a-z]_?)+)_\$` | -| `testIdAttribute` | No | `data-testid` | A string (or array of strings) used to specify the attribute used for querying by ID. This is only required if data-testid has been explicitly overridden in the [RTL configuration](https://testing-library.com/docs/dom-testing-library/api-queries#overriding-data-testid) | `data-my-test-attribute`, `["data-testid", "testId"]` | +| Option | Required | Default | Details | Example | +| ----------------- | -------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `testIdPattern` | Yes | None | A regex used to validate the format of the `data-testid` value. `{fileName}` can optionally be used as a placeholder and will be substituted with the name of the file OR the name of the files parent directory in the case when the file name is `index.js` OR empty string in the case of dynamically changing routes (that contain square brackets) with `Gatsby.js` or `Next.js` | `^{fileName}(\_\_([A-Z]+[a-z]_?)+)_\$` | +| `testIdAttribute` | No | `data-testid` | A string (or array of strings) used to specify the attribute used for querying by ID. This is only required if data-testid has been explicitly overridden in the [RTL configuration](https://testing-library.com/docs/dom-testing-library/api-queries#overriding-data-testid) | `data-my-test-attribute`, `["data-testid", "testId"]` | +| `customMessage` | No | `undefined` | A string used to display a custom message whenever warnings/errors are reported. | `A custom message` | ## Example -```json -{ - "testing-library/consistent-data-testid": [ - 2, - { - "testIdPattern": "^TestId(__[A-Z]*)?$" - } - ] -} +```js +module.exports = { + rules: { + 'testing-library/consistent-data-testid': [ + 'error', + { testIdPattern: '^TestId(__[A-Z]*)?$' }, + ], + }, +}; ``` -```json -{ - "testing-library/consistent-data-testid": [ - 2, - { - "testIdAttribute": ["data-testid", "testId"] - } - ] -} +```js +module.exports = { + rules: { + 'testing-library/consistent-data-testid': [ + 'error', + { testIdAttribute: ['data-testid', 'testId'] }, + ], + }, +}; ``` + +```js +module.exports = { + rules: { + 'testing-library/consistent-data-testid': [ + 'error', + { customMessage: 'A custom message' }, + ], + }, +}; +``` + +## Notes + +- If you are using Gatsby.js's [client-only routes](https://www.gatsbyjs.com/docs/reference/routing/file-system-route-api/#syntax-client-only-routes) or Next.js's [dynamic routes](https://nextjs.org/docs/routing/dynamic-routes) and therefore have square brackets (`[]`) in the filename (e.g. `../path/to/[component].js`), the `{fileName}` placeholder will be replaced with an empty string. This is because a linter cannot know what the dynamic content will be at run time. diff --git a/docs/rules/no-await-sync-events.md b/docs/rules/no-await-sync-events.md index 58206373..8e953882 100644 --- a/docs/rules/no-await-sync-events.md +++ b/docs/rules/no-await-sync-events.md @@ -1,10 +1,14 @@ # Disallow unnecessary `await` for sync events (`testing-library/no-await-sync-events`) -Ensure that sync simulated events are not awaited unnecessarily. +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `react`. + + + +Ensure that sync events are not awaited unnecessarily. ## Rule Details -Methods for simulating events in Testing Library ecosystem -`fireEvent` and `userEvent`- +Methods for simulating events in Testing Library ecosystem -`fireEvent` and `userEvent` prior to v14 - do NOT return any Promise, with an exception of `userEvent.type` and `userEvent.keyboard`, which delays the promise resolve only if [`delay` option](https://github.com/testing-library/user-event#typeelement-text-options) is specified. @@ -13,31 +17,39 @@ Some examples of simulating events not returning any Promise are: - `fireEvent.click` - `fireEvent.select` -- `userEvent.tab` -- `userEvent.hover` +- `userEvent.tab` (prior to `user-event` v14) +- `userEvent.hover` (prior to `user-event` v14) This rule aims to prevent users from waiting for those function calls. +> ⚠️ `fire-event` methods are async only on following Testing Library packages: +> +> - `@testing-library/vue` (supported by this plugin) +> - `@testing-library/svelte` (not supported yet by this plugin) +> - `@marko/testing-library` (supported by this plugin) + Examples of **incorrect** code for this rule: ```js const foo = async () => { - // ... - await fireEvent.click(button); - // ... + // ... + await fireEvent.click(button); + // ... }; const bar = async () => { - // ... - await userEvent.tab(); - // ... + // ... + // userEvent prior to v14 + await userEvent.tab(); + // ... }; const baz = async () => { - // ... - await userEvent.type(textInput, 'abc'); - await userEvent.keyboard('abc'); - // ... + // ... + // userEvent prior to v14 + await userEvent.type(textInput, 'abc'); + await userEvent.keyboard('abc'); + // ... }; ``` @@ -45,30 +57,62 @@ Examples of **correct** code for this rule: ```js const foo = () => { - // ... - fireEvent.click(button); - // ... + // ... + fireEvent.click(button); + // ... }; const bar = () => { - // ... - userEvent.tab(); - // ... + // ... + userEvent.tab(); + // ... }; const baz = async () => { - // await userEvent.type only with delay option - await userEvent.type(textInput, 'abc', { delay: 1000 }); - userEvent.type(textInput, '123'); - - // same for userEvent.keyboard - await userEvent.keyboard(textInput, 'abc', { delay: 1000 }); - userEvent.keyboard('123'); - // ... + // await userEvent.type only with delay option + await userEvent.type(textInput, 'abc', { delay: 1000 }); + userEvent.type(textInput, '123'); + + // same for userEvent.keyboard + await userEvent.keyboard(textInput, 'abc', { delay: 1000 }); + userEvent.keyboard('123'); + // ... +}; + +const qux = async () => { + // userEvent v14 + await userEvent.tab(); + await userEvent.click(button); + await userEvent.type(textInput, 'abc'); + await userEvent.keyboard('abc'); + // ... }; ``` +## Options + +This rule provides the following options: + +- `eventModules`: array of strings. Defines which event module should be linted for sync event methods. The possibilities are: `"fire-event"` and `"user-event"`. Defaults to `["fire-event"]`. + +### Example: + +```js +module.exports = { + rules: { + 'testing-library/no-await-sync-events': [ + 'error', + { eventModules: ['fire-event', 'user-event'] }, + ], + }, +}; +``` + +## When Not To Use It + +- `"fire-event"` option: should be disabled only for those Testing Library packages where fire-event methods are async. +- `"user-event"` option: should be disabled only if using v14 or greater. + ## Notes -There is another rule `await-fire-event`, which is only in Vue Testing -Library. Please do not confuse with this rule. +There is another rule `await-async-events`, which is for awaiting async events for `user-event` v14 or `fire-event` only in Testing Library packages with async methods. Please do not confuse with this rule. diff --git a/docs/rules/no-await-sync-query.md b/docs/rules/no-await-sync-queries.md similarity index 76% rename from docs/rules/no-await-sync-query.md rename to docs/rules/no-await-sync-queries.md index 09424258..50f639b5 100644 --- a/docs/rules/no-await-sync-query.md +++ b/docs/rules/no-await-sync-queries.md @@ -1,4 +1,8 @@ -# Disallow unnecessary `await` for sync queries (`testing-library/no-await-sync-query`) +# Disallow unnecessary `await` for sync queries (`testing-library/no-await-sync-queries`) + +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + + Ensure that sync queries are not awaited unnecessarily. @@ -40,20 +44,20 @@ Examples of **correct** code for this rule: ```js const foo = () => { - // ... - const rows = queryAllByRole('row'); - // ... + // ... + const rows = queryAllByRole('row'); + // ... }; const bar = () => { - // ... - const button = getByText('submit'); - // ... + // ... + const button = getByText('submit'); + // ... }; const baz = () => { - // ... - const button = screen.getByText('submit'); + // ... + const button = screen.getByText('submit'); }; ``` diff --git a/docs/rules/no-container.md b/docs/rules/no-container.md index c764c641..78372a4f 100644 --- a/docs/rules/no-container.md +++ b/docs/rules/no-container.md @@ -1,5 +1,9 @@ # Disallow the use of `container` methods (`testing-library/no-container`) +πŸ’Ό This rule is enabled in the following configs: `angular`, `marko`, `react`, `svelte`, `vue`. + + + By using `container` methods like `.querySelector` you may lose a lot of the confidence that the user can really interact with your UI. Also, the test becomes harder to read, and it will break more frequently. This applies to Testing Library frameworks built on top of **DOM Testing Library** diff --git a/docs/rules/no-debugging-utils.md b/docs/rules/no-debugging-utils.md index 7737eb9f..3d014d5e 100644 --- a/docs/rules/no-debugging-utils.md +++ b/docs/rules/no-debugging-utils.md @@ -1,4 +1,8 @@ -# Disallow the use of debugging utilities (`testing-library/no-debugging-utils`) +# Disallow the use of debugging utilities like `debug` (`testing-library/no-debugging-utils`) + +⚠️ This rule _warns_ in the following configs: `angular`, `marko`, `react`, `svelte`, `vue`. + + Just like `console.log` statements pollutes the browser's output, debug statements also pollutes the tests if one of your teammates forgot to remove it. `debug` statements should be used when you actually want to debug your tests but should not be pushed to the codebase. @@ -13,7 +17,7 @@ This rule supports disallowing the following debugging utilities: - `logDOM` - `prettyFormat` -By default, only `debug` and `logTestingPlaygroundURL` are disallowed. +By default, all are disallowed. Examples of **incorrect** code for this rule: @@ -37,21 +41,25 @@ const { screen } = require('@testing-library/react'); screen.debug(); ``` +## Options + You can control which debugging utils are checked for with the `utilsToCheckFor` option: -```json -{ - "testing-library/no-debugging-utils": [ - "error", - { - "utilsToCheckFor": { - "debug": false, - "logRoles": true, - "logDOM": true - } - } - ] -} +```js +module.exports = { + rules: { + 'testing-library/no-debugging-utils': [ + 'error', + { + utilsToCheckFor: { + debug: false, + logRoles: true, + logDOM: true, + }, + }, + ], + }, +}; ``` ## Further Reading diff --git a/docs/rules/no-dom-import.md b/docs/rules/no-dom-import.md index a7ba4e1b..ad6b97a4 100644 --- a/docs/rules/no-dom-import.md +++ b/docs/rules/no-dom-import.md @@ -1,5 +1,11 @@ # Disallow importing from DOM Testing Library (`testing-library/no-dom-import`) +πŸ’Ό This rule is enabled in the following configs: `angular`, `marko`, `react`, `svelte`, `vue`. + +πŸ”§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + Ensure that there are no direct imports from `@testing-library/dom` or `dom-testing-library` when using some testing library framework wrapper. @@ -31,6 +37,11 @@ import { fireEvent } from 'dom-testing-library'; import { fireEvent } from '@testing-library/dom'; ``` +```js +import { render } from '@testing-library/react'; // Okay, no error +import { screen } from '@testing-library/dom'; // Error, unnecessary import from @testing-library/dom +``` + ```js const { fireEvent } = require('dom-testing-library'); ``` @@ -63,10 +74,12 @@ This rule has an option in case you want to tell the user which framework to use ### Example -```json -{ - "testing-library/no-dom-import": ["error", "react"] -} +```js +module.exports = { + rules: { + 'testing-library/no-dom-import': ['error', 'react'], + }, +}; ``` With the configuration above, if the user imports from `@testing-library/dom` or `dom-testing-library` instead of the used framework, ESLint will tell the user to import from `@testing-library/react` or `react-testing-library`. @@ -76,3 +89,4 @@ With the configuration above, if the user imports from `@testing-library/dom` or - [Angular Testing Library API](https://testing-library.com/docs/angular-testing-library/api) - [React Testing Library API](https://testing-library.com/docs/react-testing-library/api) - [Vue Testing Library API](https://testing-library.com/docs/vue-testing-library/api) +- [Marko Testing Library API](https://testing-library.com/docs/marko-testing-library/api) diff --git a/docs/rules/no-global-regexp-flag-in-query.md b/docs/rules/no-global-regexp-flag-in-query.md new file mode 100644 index 00000000..5bca5f1c --- /dev/null +++ b/docs/rules/no-global-regexp-flag-in-query.md @@ -0,0 +1,37 @@ +# Disallow the use of the global RegExp flag (/g) in queries (`testing-library/no-global-regexp-flag-in-query`) + +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + +πŸ”§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +Ensure that there are no global RegExp flags used when using queries. + +## Rule Details + +A RegExp instance that's using the global flag `/g` holds state and this might cause false-positives while querying for elements. + +Examples of **incorrect** code for this rule: + +```js +screen.getByText(/hello/gi); +``` + +```js +await screen.findByRole('button', { otherProp: true, name: /hello/g }); +``` + +Examples of **correct** code for this rule: + +```js +screen.getByText(/hello/i); +``` + +```js +await screen.findByRole('button', { otherProp: true, name: /hello/ }); +``` + +## Further Reading + +- [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex) diff --git a/docs/rules/no-manual-cleanup.md b/docs/rules/no-manual-cleanup.md index 4b63f88f..69681cba 100644 --- a/docs/rules/no-manual-cleanup.md +++ b/docs/rules/no-manual-cleanup.md @@ -1,5 +1,9 @@ # Disallow the use of `cleanup` (`testing-library/no-manual-cleanup`) +πŸ’Ό This rule is enabled in the following configs: `react`, `svelte`, `vue`. + + + `cleanup` is performed automatically if the testing framework you're using supports the `afterEach` global (like mocha, Jest, and Jasmine). In this case, it's unnecessary to do manual cleanups after each test unless you skip the auto-cleanup with environment variables such as `RTL_SKIP_AUTO_CLEANUP` for React. ## Rule Details diff --git a/docs/rules/no-node-access.md b/docs/rules/no-node-access.md index 638f06eb..3ae667b7 100644 --- a/docs/rules/no-node-access.md +++ b/docs/rules/no-node-access.md @@ -1,10 +1,18 @@ # Disallow direct Node access (`testing-library/no-node-access`) -The Testing Library already provides methods for querying DOM elements. +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + + + +Disallow direct access or manipulation of DOM nodes in favor of Testing Library's user-centric APIs. ## Rule Details -This rule aims to disallow DOM traversal using native HTML methods and properties, such as `closest`, `lastChild` and all that returns another Node element from an HTML tree. +This rule aims to disallow direct access and manipulation of DOM nodes using native HTML properties and methods β€” including traversal (e.g. `closest`, `lastChild`) as well as direct actions (e.g. `click()`, `select()`). Use Testing Library’s queries and userEvent APIs instead. + +> [!NOTE] +> This rule does not report usage of `focus()` or `blur()`, because imperative usage (e.g. `getByText('focus me').focus()` or .`blur()`) is recommended over `fireEvent.focus()` or `fireEvent.blur()`. +> If an element is not focusable, related assertions will fail, leading to more robust tests. See [Testing Library Events Guide](https://testing-library.com/docs/guide-events/) for more details. Examples of **incorrect** code for this rule: @@ -17,6 +25,12 @@ screen.getByText('Submit').closest('button'); // chaining with Testing Library m ```js import { screen } from '@testing-library/react'; +screen.getByText('Submit').click(); +``` + +```js +import { screen } from '@testing-library/react'; + const buttons = screen.getAllByRole('button'); expect(buttons[1].lastChild).toBeInTheDocument(); ``` @@ -37,6 +51,12 @@ const button = screen.getByRole('button'); expect(button).toHaveTextContent('submit'); ``` +```js +import { screen } from '@testing-library/react'; + +userEvent.click(screen.getByText('Submit')); +``` + ```js import { render, within } from '@testing-library/react'; @@ -45,12 +65,42 @@ const signinModal = getByLabelText('Sign In'); within(signinModal).getByPlaceholderText('Username'); ``` +```js +import { screen } from '@testing-library/react'; + +function ComponentA(props) { + // props.children is not reported + return
{props.children}
; +} + +render(); +``` + ```js // If is not importing a testing-library package document.getElementById('submit-btn').closest('button'); ``` +## Options + +This rule has one option: + +- `allowContainerFirstChild`: **disabled by default**. When we have container + with rendered content then the easiest way to access content itself is [by using + `firstChild` property](https://testing-library.com/docs/react-testing-library/api/#container-1). Use this option in cases when this is hardly avoidable. + + ```js + "testing-library/no-node-access": ["error", {"allowContainerFirstChild": true}] + ``` + +Correct: + +```jsx +const { container } = render(); +expect(container.firstChild).toMatchSnapshot(); +``` + ## Further Reading ### Properties / methods that return another Node @@ -58,3 +108,7 @@ document.getElementById('submit-btn').closest('button'); - [`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document) - [`Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element) - [`Node`](https://developer.mozilla.org/en-US/docs/Web/API/Node) + +### Testing Library Guides + +- [Testing Library Events Guide](https://testing-library.com/docs/guide-events/) diff --git a/docs/rules/no-promise-in-fire-event.md b/docs/rules/no-promise-in-fire-event.md index 3bd5dd5f..e9bf9b42 100644 --- a/docs/rules/no-promise-in-fire-event.md +++ b/docs/rules/no-promise-in-fire-event.md @@ -1,5 +1,9 @@ # Disallow the use of promises passed to a `fireEvent` method (`testing-library/no-promise-in-fire-event`) +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + + + Methods from `fireEvent` expect to receive a DOM element. Passing a promise will end up in an error, so it must be prevented. Examples of **incorrect** code for this rule: diff --git a/docs/rules/no-render-in-lifecycle.md b/docs/rules/no-render-in-lifecycle.md new file mode 100644 index 00000000..bbbea3d8 --- /dev/null +++ b/docs/rules/no-render-in-lifecycle.md @@ -0,0 +1,91 @@ +# Disallow the use of `render` in testing frameworks setup functions (`testing-library/no-render-in-lifecycle`) + +πŸ’Ό This rule is enabled in the following configs: `angular`, `marko`, `react`, `svelte`, `vue`. + + + +## Rule Details + +This rule disallows the usage of `render` (or a custom render function) in testing framework setup functions (`beforeEach` and `beforeAll`) in favor of moving `render` closer to test assertions. + +This rule reduces the amount of variable mutation, in particular avoiding nesting `beforeEach` functions. According to Kent C. Dodds, that results in vastly simpler test maintenance. + +For more background on the origin and rationale for this best practice, read Kent C. Dodds's [Avoid Nesting when you're Testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing). + +Examples of **incorrect** code for this rule: + +```js +beforeEach(() => { + render(); +}); + +it('Should have foo', () => { + expect(screen.getByText('foo')).toBeInTheDocument(); +}); + +it('Should have bar', () => { + expect(screen.getByText('bar')).toBeInTheDocument(); +}); +``` + +```js +const setup = () => render(); + +beforeEach(() => { + setup(); +}); + +it('Should have foo', () => { + expect(screen.getByText('foo')).toBeInTheDocument(); +}); + +it('Should have bar', () => { + expect(screen.getByText('bar')).toBeInTheDocument(); +}); +``` + +```js +beforeAll(() => { + render(); +}); + +it('Should have foo', () => { + expect(screen.getByText('foo')).toBeInTheDocument(); +}); + +it('Should have bar', () => { + expect(screen.getByText('bar')).toBeInTheDocument(); +}); +``` + +Examples of **correct** code for this rule: + +```js +it('Should have foo and bar', () => { + render(); + expect(screen.getByText('foo')).toBeInTheDocument(); + expect(screen.getByText('bar')).toBeInTheDocument(); +}); +``` + +```js +const setup = () => render(); + +beforeEach(() => { + // other stuff... +}); + +it('Should have foo and bar', () => { + setup(); + expect(screen.getByText('foo')).toBeInTheDocument(); + expect(screen.getByText('bar')).toBeInTheDocument(); +}); +``` + +## Options + +If you would like to allow the use of `render` (or a custom render function) in _either_ `beforeAll` or `beforeEach`, this can be configured using the option `allowTestingFrameworkSetupHook`. This may be useful if you have configured your tests to [skip auto cleanup](https://testing-library.com/docs/react-testing-library/setup#skipping-auto-cleanup). `allowTestingFrameworkSetupHook` is an enum that accepts either `"beforeAll"` or `"beforeEach"`. + +``` + "testing-library/no-render-in-lifecycle": ["error", {"allowTestingFrameworkSetupHook": "beforeAll"}], +``` diff --git a/docs/rules/no-render-in-setup.md b/docs/rules/no-render-in-setup.md deleted file mode 100644 index 91ec364f..00000000 --- a/docs/rules/no-render-in-setup.md +++ /dev/null @@ -1,81 +0,0 @@ -# Disallow the use of `render` in setup functions (`testing-library/no-render-in-setup`) - -## Rule Details - -This rule disallows the usage of `render` (or a custom render function) in testing framework setup functions (`beforeEach` and `beforeAll`) in favor of moving `render` closer to test assertions. - -Examples of **incorrect** code for this rule: - -```js -beforeEach(() => { - render(); -}); - -it('Should have foo', () => { - expect(screen.getByText('foo')).toBeInTheDocument(); -}); - -it('Should have bar', () => { - expect(screen.getByText('bar')).toBeInTheDocument(); -}); -``` - -```js -const setup = () => render(); - -beforeEach(() => { - setup(); -}); - -it('Should have foo', () => { - expect(screen.getByText('foo')).toBeInTheDocument(); -}); - -it('Should have bar', () => { - expect(screen.getByText('bar')).toBeInTheDocument(); -}); -``` - -```js -beforeAll(() => { - render(); -}); - -it('Should have foo', () => { - expect(screen.getByText('foo')).toBeInTheDocument(); -}); - -it('Should have bar', () => { - expect(screen.getByText('bar')).toBeInTheDocument(); -}); -``` - -Examples of **correct** code for this rule: - -```js -it('Should have foo and bar', () => { - render(); - expect(screen.getByText('foo')).toBeInTheDocument(); - expect(screen.getByText('bar')).toBeInTheDocument(); -}); -``` - -```js -const setup = () => render(); - -beforeEach(() => { - // other stuff... -}); - -it('Should have foo and bar', () => { - setup(); - expect(screen.getByText('foo')).toBeInTheDocument(); - expect(screen.getByText('bar')).toBeInTheDocument(); -}); -``` - -If you would like to allow the use of `render` (or a custom render function) in _either_ `beforeAll` or `beforeEach`, this can be configured using the option `allowTestingFrameworkSetupHook`. This may be useful if you have configured your tests to [skip auto cleanup](https://testing-library.com/docs/react-testing-library/setup#skipping-auto-cleanup). `allowTestingFrameworkSetupHook` is an enum that accepts either `"beforeAll"` or `"beforeEach"`. - -``` - "testing-library/no-render-in-setup": ["error", {"allowTestingFrameworkSetupHook": "beforeAll"}], -``` diff --git a/docs/rules/no-test-id-queries.md b/docs/rules/no-test-id-queries.md new file mode 100644 index 00000000..958b6bf7 --- /dev/null +++ b/docs/rules/no-test-id-queries.md @@ -0,0 +1,32 @@ +# Ensure no `data-testid` queries are used (`testing-library/no-test-id-queries`) + + + +## Rule Details + +This rule aims to reduce the usage of `*ByTestId` queries in your tests. + +When using `*ByTestId` queries, you are coupling your tests to the implementation details of your components, and not to how they behave and being used. + +Prefer using queries that are more related to the user experience, like `getByRole`, `getByLabelText`, etc. + +Example of **incorrect** code for this rule: + +```js +const button = queryByTestId('my-button'); +const input = screen.queryByTestId('my-input'); +``` + +Examples of **correct** code for this rule: + +```js +const button = screen.getByRole('button'); +const input = screen.getByRole('textbox'); +``` + +## Further Reading + +- [about `getByTestId`](https://testing-library.com/docs/queries/bytestid) +- [about `getByRole`](https://testing-library.com/docs/queries/byrole) +- [about `getByLabelText`](https://testing-library.com/docs/queries/bylabeltext) +- [Common mistakes with React Testing Library - Not querying by text](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-querying-by-text) diff --git a/docs/rules/no-unnecessary-act.md b/docs/rules/no-unnecessary-act.md index 7b8fd0f1..3b0d2bcb 100644 --- a/docs/rules/no-unnecessary-act.md +++ b/docs/rules/no-unnecessary-act.md @@ -1,10 +1,15 @@ # Disallow wrapping Testing Library utils or empty callbacks in `act` (`testing-library/no-unnecessary-act`) +πŸ’Ό This rule is enabled in the following configs: `marko`, `react`. + + + > ⚠️ The `act` method is only available on the following Testing Library packages: > > - `@testing-library/react` (supported by this plugin) > - `@testing-library/preact` (not supported yet by this plugin) > - `@testing-library/svelte` (not supported yet by this plugin) +> - `@marko/testing-library` (supported by this plugin) ## Rule Details @@ -21,11 +26,11 @@ Example of **incorrect** code for this rule: ```js // ❌ wrapping things related to Testing Library in `act` is incorrect import { - act, - render, - screen, - waitFor, - fireEvent, + act, + render, + screen, + waitFor, + fireEvent, } from '@testing-library/react'; // ^ act imported from 'react-dom/test-utils' will be reported too import userEvent from '@testing-library/user-event'; @@ -33,7 +38,7 @@ import userEvent from '@testing-library/user-event'; // ... act(() => { - render(); + render(); }); await act(async () => waitFor(() => {})); @@ -41,11 +46,11 @@ await act(async () => waitFor(() => {})); act(() => screen.getByRole('button')); act(() => { - fireEvent.click(element); + fireEvent.click(element); }); act(() => { - userEvent.click(element); + userEvent.click(element); }); ``` @@ -72,7 +77,7 @@ import { stuffThatDoesNotUseRTL } from 'somwhere-else'; // ... act(() => { - stuffThatDoesNotUseRTL(); + stuffThatDoesNotUseRTL(); }); ``` @@ -82,8 +87,8 @@ import { act, screen } from '@testing-library/react'; import { stuffThatDoesNotUseRTL } from 'somwhere-else'; await act(async () => { - await screen.findByRole('button'); - stuffThatDoesNotUseRTL(); + await screen.findByRole('button'); + stuffThatDoesNotUseRTL(); }); ``` @@ -91,7 +96,7 @@ await act(async () => { This rule has one option: -- `isStrict`: **disabled by default**. Wrapping both things related and not related to Testing Library in `act` is reported +- `isStrict`: **enabled by default**. Wrapping both things related and not related to Testing Library in `act` is reported ```js "testing-library/no-unnecessary-act": ["error", {"isStrict": true}] @@ -106,8 +111,8 @@ import { act, screen } from '@testing-library/react'; import { stuffThatDoesNotUseRTL } from 'somwhere-else'; await act(async () => { - await screen.findByRole('button'); - stuffThatDoesNotUseRTL(); + await screen.findByRole('button'); + stuffThatDoesNotUseRTL(); }); ``` diff --git a/docs/rules/no-wait-for-empty-callback.md b/docs/rules/no-wait-for-empty-callback.md deleted file mode 100644 index 612af0f9..00000000 --- a/docs/rules/no-wait-for-empty-callback.md +++ /dev/null @@ -1,41 +0,0 @@ -# Empty callbacks inside `waitFor` and `waitForElementToBeRemoved` are not preferred (`testing-library/no-wait-for-empty-callback`) - -## Rule Details - -This rule aims to ensure the correct usage of `waitFor` and `waitForElementToBeRemoved`, in the way that they're intended to be used. -If an empty callback is passed, these methods will just wait next tick of the event loop before proceeding, and that's not consistent with the philosophy of the library. -**Instead, insert an assertion in that callback function.** - -Examples of **incorrect** code for this rule: - -```js -const foo = async () => { - await waitFor(() => {}); - await waitFor(function () {}); - await waitFor(noop); - - await waitForElementToBeRemoved(() => {}); - await waitForElementToBeRemoved(function () {}); - await waitForElementToBeRemoved(noop); -}; -``` - -Examples of **correct** code for this rule: - -```js -const foo = async () => { - await waitFor(() => { - screen.getByText(/submit/i); - }); - - const submit = screen.getByText(/submit/i); - await waitForElementToBeRemoved(() => submit); - // or - await waitForElementToBeRemoved(submit); -}; -``` - -## Further Reading - -- [dom-testing-library v7 release](https://github.com/testing-library/dom-testing-library/releases/tag/v7.0.0) -- [inspiration for this rule](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#passing-an-empty-callback-to-waitfor) diff --git a/docs/rules/no-wait-for-multiple-assertions.md b/docs/rules/no-wait-for-multiple-assertions.md index efe376ca..83a3bd51 100644 --- a/docs/rules/no-wait-for-multiple-assertions.md +++ b/docs/rules/no-wait-for-multiple-assertions.md @@ -1,9 +1,13 @@ -# Disallow the use of multiple expect inside `waitFor` (`testing-library/no-wait-for-multiple-assertions`) +# Disallow the use of multiple `expect` calls inside `waitFor` (`testing-library/no-wait-for-multiple-assertions`) + +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + + ## Rule Details This rule aims to ensure the correct usage of `expect` inside `waitFor`, in the way that they're intended to be used. -When using multiples assertions inside `waitFor`, if one fails, you have to wait for a timeout before seeing it failing. +When using multiple assertions inside `waitFor`, if one fails, you have to wait for a timeout before seeing it failing. Putting one assertion, you can both wait for the UI to settle to the state you want to assert on, and also fail faster if one of the assertions do end up failing @@ -11,16 +15,16 @@ Example of **incorrect** code for this rule: ```js const foo = async () => { - await waitFor(() => { - expect(a).toEqual('a'); - expect(b).toEqual('b'); - }); - - // or - await waitFor(function () { - expect(a).toEqual('a'); - expect(b).toEqual('b'); - }); + await waitFor(() => { + expect(a).toEqual('a'); + expect(b).toEqual('b'); + }); + + // or + await waitFor(function () { + expect(a).toEqual('a'); + expect(b).toEqual('b'); + }); }; ``` @@ -28,21 +32,21 @@ Examples of **correct** code for this rule: ```js const foo = async () => { - await waitFor(() => expect(a).toEqual('a')); - expect(b).toEqual('b'); - - // or - await waitFor(function () { - expect(a).toEqual('a'); - }); - expect(b).toEqual('b'); - - // it only detects expect - // so this case doesn't generate warnings - await waitFor(() => { - fireEvent.keyDown(input, { key: 'ArrowDown' }); - expect(b).toEqual('b'); - }); + await waitFor(() => expect(a).toEqual('a')); + expect(b).toEqual('b'); + + // or + await waitFor(function () { + expect(a).toEqual('a'); + }); + expect(b).toEqual('b'); + + // it only detects expect + // so this case doesn't generate warnings + await waitFor(() => { + fireEvent.keyDown(input, { key: 'ArrowDown' }); + expect(b).toEqual('b'); + }); }; ``` diff --git a/docs/rules/no-wait-for-side-effects.md b/docs/rules/no-wait-for-side-effects.md index b545fae5..08cc31f1 100644 --- a/docs/rules/no-wait-for-side-effects.md +++ b/docs/rules/no-wait-for-side-effects.md @@ -1,4 +1,8 @@ -# Disallow the use of side effects inside `waitFor` (`testing-library/no-wait-for-side-effects`) +# Disallow the use of side effects in `waitFor` (`testing-library/no-wait-for-side-effects`) + +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + + ## Rule Details @@ -73,6 +77,15 @@ Examples of **correct** code for this rule: expect(b).toEqual('b'); }); + // or + userEvent.click(button); + waitFor(function() { + expect(b).toEqual('b'); + }).then(() => { + // Outside of waitFor, e.g. inside a .then() side effects are allowed + fireEvent.click(button); + }); + // or render() await waitFor(() => { diff --git a/docs/rules/no-wait-for-snapshot.md b/docs/rules/no-wait-for-snapshot.md index 65b3683f..71b2fd49 100644 --- a/docs/rules/no-wait-for-snapshot.md +++ b/docs/rules/no-wait-for-snapshot.md @@ -1,4 +1,8 @@ -# Ensures no snapshot is generated inside a `waitFor` call (`testing-library/no-wait-for-snapshot`) +# Ensures no snapshot is generated inside of a `waitFor` call (`testing-library/no-wait-for-snapshot`) + +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + + Ensure that no calls to `toMatchSnapshot` or `toMatchInlineSnapshot` are made from within a `waitFor` method (or any of the other async utility methods). @@ -15,23 +19,15 @@ Examples of **incorrect** code for this rule: ```js const foo = async () => { - // ... - await waitFor(() => expect(container).toMatchSnapshot()); - // ... + // ... + await waitFor(() => expect(container).toMatchSnapshot()); + // ... }; const bar = async () => { - // ... - await waitFor(() => expect(container).toMatchInlineSnapshot()); - // ... -}; - -const baz = async () => { - // ... - await wait(() => { - expect(container).toMatchSnapshot(); - }); - // ... + // ... + await waitFor(() => expect(container).toMatchInlineSnapshot()); + // ... }; ``` @@ -39,15 +35,15 @@ Examples of **correct** code for this rule: ```js const foo = () => { - // ... - expect(container).toMatchSnapshot(); - // ... + // ... + expect(container).toMatchSnapshot(); + // ... }; const bar = () => { - // ... - expect(container).toMatchInlineSnapshot(); - // ... + // ... + expect(container).toMatchInlineSnapshot(); + // ... }; ``` diff --git a/docs/rules/prefer-explicit-assert.md b/docs/rules/prefer-explicit-assert.md index 1937dc30..df021751 100644 --- a/docs/rules/prefer-explicit-assert.md +++ b/docs/rules/prefer-explicit-assert.md @@ -1,4 +1,6 @@ -# Suggest using explicit assertions rather than just `getBy*` queries (`testing-library/prefer-explicit-assert`) +# Suggest using explicit assertions rather than standalone queries (`testing-library/prefer-explicit-assert`) + + Testing Library `getBy*` queries throw an error if the element is not found. Some users like this behavior to use the query itself as an @@ -70,7 +72,10 @@ This is how you can use these options in eslint configuration: ## When Not To Use It -If you prefer to use `getBy*` queries implicitly as an assert-like method itself, then this rule is not recommended. +If you prefer to use `getBy*` queries implicitly as an assert-like method itself, then this rule is not recommended. Instead check out this rule [prefer-implicit-assert](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-implicit-assert.md) + +- Never use both `prefer-explicit-assert` & `prefer-implicit-assert` choose one. +- This library recommends `prefer-explicit-assert` to make it more clear to readers that it is not just a query without an assertion, but that it is checking for existence of an element ## Further Reading diff --git a/docs/rules/prefer-find-by.md b/docs/rules/prefer-find-by.md index 210e4c8c..0431e27b 100644 --- a/docs/rules/prefer-find-by.md +++ b/docs/rules/prefer-find-by.md @@ -1,40 +1,52 @@ -# Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries (`testing-library/prefer-find-by`) +# Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements (`testing-library/prefer-find-by`) -findBy* queries are a simple combination of getBy* queries and waitFor. The findBy\* queries accept the waitFor options as the last argument. (i.e. screen.findByText('text', queryOptions, waitForOptions)) +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + +πŸ”§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +`findBy*` queries are a simple combination of `getBy*` queries and `waitFor`. The `findBy*` queries accept the `waitFor` options as the last argument. (i.e. `screen.findByText('text', queryOptions, waitForOptions)`) ## Rule details -This rule aims to use `findBy*` or `findAllBy*` queries to wait for elements, rather than using `waitFor`, or the deprecated methods `waitForElement` and `wait`. -This rule analyzes those cases where `waitFor` is used with just one query method, in the form of an arrow function with only one statement (that is, without a block of statements). Given the callback could be more complex, this rule does not consider function callbacks or arrow functions with blocks of code +This rule aims to use `findBy*` or `findAllBy*` queries to wait for elements, rather than using `waitFor`. +This rule analyzes those cases where `waitFor` is used with just one query method, in the form of an arrow function with only one statement (that is, without a block of statements). Given the callback could be more complex, this rule does not consider function callbacks or arrow functions with blocks of code. Examples of **incorrect** code for this rule ```js // arrow functions with one statement, using screen and any sync query method const submitButton = await waitFor(() => - screen.getByRole('button', { name: /submit/i }) + screen.getByRole('button', { name: /submit/i }) ); const submitButton = await waitFor(() => - screen.getAllByTestId('button', { name: /submit/i }) + screen.getAllByTestId('button', { name: /submit/i }) ); // arrow functions with one statement, calling any sync query method const submitButton = await waitFor(() => - queryByLabel('button', { name: /submit/i }) + queryByLabel('button', { name: /submit/i }) ); const submitButton = await waitFor(() => - queryAllByText('button', { name: /submit/i }) + queryAllByText('button', { name: /submit/i }) ); // arrow functions with one statement, calling any sync query method with presence assertion const submitButton = await waitFor(() => - expect(queryByLabel('button', { name: /submit/i })).toBeInTheDocument() + expect(queryByLabel('button', { name: /submit/i })).toBeInTheDocument() ); const submitButton = await waitFor(() => - expect(queryByLabel('button', { name: /submit/i })).not.toBeFalsy() + expect(queryByLabel('button', { name: /submit/i })).not.toBeFalsy() ); + +// unnecessary usage of waitFor with findBy*, which already includes waiting logic +await waitFor(async () => { + const button = await findByRole('button', { name: 'Submit' }); + expect(button).toBeInTheDocument(); +}); ``` Examples of **correct** code for this rule: @@ -51,21 +63,21 @@ await waitForElementToBeRemoved(document.querySelector('foo')); // using waitFor with a function await waitFor(function () { - foo(); - return getByText('name'); + foo(); + return getByText('name'); }); // passing a reference of a function function myCustomFunction() { - foo(); - return getByText('name'); + foo(); + return getByText('name'); } await waitFor(myCustomFunction); // using waitFor with an arrow function with a code block await waitFor(() => { - baz(); - return queryAllByText('foo'); + baz(); + return queryAllByText('foo'); }); // using a custom arrow function @@ -78,7 +90,7 @@ await waitFor(() => expect(getAllByText('bar')).toBeDisabled()); ## When Not To Use It -- Not encouraging use of findBy shortcut from testing library best practices +- Not encouraging use of `findBy` shortcut from testing library best practices ## Further Reading diff --git a/docs/rules/prefer-implicit-assert.md b/docs/rules/prefer-implicit-assert.md new file mode 100644 index 00000000..fc9323d7 --- /dev/null +++ b/docs/rules/prefer-implicit-assert.md @@ -0,0 +1,57 @@ +# Suggest using implicit assertions for getBy* & findBy* queries (`testing-library/prefer-implicit-assert`) + + + +Testing Library `getBy*` & `findBy*` queries throw an error if the element is not +found. Therefore it is not necessary to also assert existence with things like `expect(getBy*.toBeInTheDocument()` or `expect(await findBy*).not.toBeNull()` + +## Rule Details + +This rule aims to reduce unnecessary assertion's for presence of an element, +when using queries that implicitly fail when said element is not found. + +Examples of **incorrect** code for this rule with the default configuration: + +```js +// wrapping the getBy or findBy queries within a `expect` and using existence matchers for +// making the assertion is not necessary +expect(getByText('foo')).toBeInTheDocument(); +expect(await findByText('foo')).toBeInTheDocument(); + +expect(getByText('foo')).toBeDefined(); +expect(await findByText('foo')).toBeDefined(); + +const utils = render(); +expect(utils.getByText('foo')).toBeInTheDocument(); +expect(await utils.findByText('foo')).toBeInTheDocument(); + +expect(await findByText('foo')).not.toBeNull(); +expect(await findByText('foo')).not.toBeUndefined(); +``` + +Examples of **correct** code for this rule with the default configuration: + +```js +getByText('foo'); +await findByText('foo'); + +const utils = render(); +utils.getByText('foo'); +await utils.findByText('foo'); + +// When using queryBy* queries these do not implicitly fail therefore you should explicitly check if your elements exist or not +expect(queryByText('foo')).toBeInTheDocument(); +expect(queryByText('foo')).not.toBeInTheDocument(); +``` + +## When Not To Use It + +If you prefer to use `getBy*` & `findBy*` queries with explicitly asserting existence of elements, then this rule is not recommended. Instead check out this rule [prefer-explicit-assert](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-explicit-assert.md) + +- Never use both `prefer-implicit-assert` & `prefer-explicit-assert` choose one. +- This library recommends `prefer-explicit-assert` to make it more clear to readers that it is not just a query without an assertion, but that it is checking for existence of an element + +## Further Reading + +- [getBy query](https://testing-library.com/docs/dom-testing-library/api-queries#getby) +- [findBy query](https://testing-library.com/docs/dom-testing-library/api-queries#findBy) diff --git a/docs/rules/prefer-presence-queries.md b/docs/rules/prefer-presence-queries.md index 05ef773a..b7e40121 100644 --- a/docs/rules/prefer-presence-queries.md +++ b/docs/rules/prefer-presence-queries.md @@ -1,4 +1,10 @@ -# Enforce specific queries when checking element is present or not (`testing-library/prefer-presence-queries`) +# Ensure appropriate `get*`/`query*` queries are used with their respective matchers (`testing-library/prefer-presence-queries`) + +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + +πŸ”§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + The (DOM) Testing Library allows to query DOM elements using different types of queries such as `get*` and `query*`. Using `get*` throws an error in case the element is not found, while `query*` returns null instead of throwing (or empty array for `queryAllBy*` ones). These differences are useful in some situations: @@ -9,28 +15,29 @@ The (DOM) Testing Library allows to query DOM elements using different types of This rule fires whenever: -- `queryBy*` or `queryAllBy*` are used to assert element **is** present with `.toBeInTheDocument()`, `toBeTruthy()` or `.toBeDefined()` matchers or negated matchers from case below. +- `queryBy*` or `queryAllBy*` are used to assert element **is** present with `.toBeInTheDocument()`, `toBeTruthy()` or `.toBeDefined()` matchers or negated matchers from case below, or when used inside a `within()` clause. - `getBy*` or `getAllBy*` are used to assert element **is not** present with `.toBeNull()` or `.toBeFalsy()` matchers or negated matchers from case above. Examples of **incorrect** code for this rule: ```js test('some test', () => { - render(); - - // check element is present with `queryBy*` - expect(screen.queryByText('button')).toBeInTheDocument(); - expect(screen.queryAllByText('button')[0]).toBeTruthy(); - expect(screen.queryByText('button')).not.toBeNull(); - expect(screen.queryAllByText('button')[2]).not.toBeNull(); - expect(screen.queryByText('button')).not.toBeFalsy(); - - // check element is NOT present with `getBy*` - expect(screen.getByText('loading')).not.toBeInTheDocument(); - expect(screen.getAllByText('loading')[1]).not.toBeTruthy(); - expect(screen.getByText('loading')).toBeNull(); - expect(screen.getAllByText('loading')[3]).toBeNull(); - expect(screen.getByText('loading')).toBeFalsy(); + render(); + + // check element is present with `queryBy*` + expect(screen.queryByText('button')).toBeInTheDocument(); + expect(screen.queryAllByText('button')[0]).toBeTruthy(); + expect(screen.queryByText('button')).not.toBeNull(); + expect(screen.queryAllByText('button')[2]).not.toBeNull(); + expect(screen.queryByText('button')).not.toBeFalsy(); + ...(within(screen.queryByText('button')))... + + // check element is NOT present with `getBy*` + expect(screen.getByText('loading')).not.toBeInTheDocument(); + expect(screen.getAllByText('loading')[1]).not.toBeTruthy(); + expect(screen.getByText('loading')).toBeNull(); + expect(screen.getAllByText('loading')[3]).toBeNull(); + expect(screen.getByText('loading')).toBeFalsy(); }); ``` @@ -38,27 +45,49 @@ Examples of **correct** code for this rule: ```js test('some test', async () => { - render(); - // check element is present with `getBy*` - expect(screen.getByText('button')).toBeInTheDocument(); - expect(screen.getAllByText('button')[9]).toBeTruthy(); - expect(screen.getByText('button')).not.toBeNull(); - expect(screen.getAllByText('button')[7]).not.toBeNull(); - expect(screen.getByText('button')).not.toBeFalsy(); - - // check element is NOT present with `queryBy*` - expect(screen.queryByText('loading')).not.toBeInTheDocument(); - expect(screen.queryAllByText('loading')[8]).not.toBeTruthy(); - expect(screen.queryByText('loading')).toBeNull(); - expect(screen.queryAllByText('loading')[6]).toBeNull(); - expect(screen.queryByText('loading')).toBeFalsy(); - - // `findBy*` queries are out of the scope for this rule - const button = await screen.findByText('submit'); - expect(button).toBeInTheDocument(); + render(); + + // check element is present with `getBy*` + expect(screen.getByText('button')).toBeInTheDocument(); + expect(screen.getAllByText('button')[9]).toBeTruthy(); + expect(screen.getByText('button')).not.toBeNull(); + expect(screen.getAllByText('button')[7]).not.toBeNull(); + expect(screen.getByText('button')).not.toBeFalsy(); + ...(within(screen.getByText('button')))... + + // check element is NOT present with `queryBy*` + expect(screen.queryByText('loading')).not.toBeInTheDocument(); + expect(screen.queryAllByText('loading')[8]).not.toBeTruthy(); + expect(screen.queryByText('loading')).toBeNull(); + expect(screen.queryAllByText('loading')[6]).toBeNull(); + expect(screen.queryByText('loading')).toBeFalsy(); + + // `findBy*` queries are out of the scope for this rule + const button = await screen.findByText('submit'); + expect(button).toBeInTheDocument(); }); ``` +## Options + +| Option | Required | Default | Details | +| ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `presence` | No | `true` | If enabled, this rule will ensure `getBy*` is used to validate whether an element is present. If disabled, `queryBy*` will be accepted for presence queries. _Note: using this option is not recommended. It is workaround for false positives that should eventually be [fixed](https://github.com/testing-library/eslint-plugin-testing-library/issues/518) in this repository._ | +| `absence` | No | `true` | If enabled, this rule will ensure `queryBy*` is used to validate whether an element is absent. If disabled, `getBy*` will be accepted for absence queries. _Note: using this option is not recommended. It is workaround for false positives that should eventually be [fixed](https://github.com/testing-library/eslint-plugin-testing-library/issues/518) in this repository._ | + +## Example + +```js +module.exports = { + rules: { + 'testing-library/prefer-presence-queries': [ + 'error', + { absence: false, presence: true }, + ], + }, +}; +``` + ## Further Reading - [Testing Library queries cheatsheet](https://testing-library.com/docs/dom-testing-library/cheatsheet#queries) diff --git a/docs/rules/prefer-query-by-disappearance.md b/docs/rules/prefer-query-by-disappearance.md index a0eabef2..6d20641f 100644 --- a/docs/rules/prefer-query-by-disappearance.md +++ b/docs/rules/prefer-query-by-disappearance.md @@ -1,5 +1,9 @@ # Suggest using `queryBy*` queries when waiting for disappearance (`testing-library/prefer-query-by-disappearance`) +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + + + ## Rule Details This rule enforces using `queryBy*` queries when waiting for disappearance with `waitForElementToBeRemoved`. diff --git a/docs/rules/prefer-query-matchers.md b/docs/rules/prefer-query-matchers.md new file mode 100644 index 00000000..eec3fa2f --- /dev/null +++ b/docs/rules/prefer-query-matchers.md @@ -0,0 +1,85 @@ +# Ensure the configured `get*`/`query*` query is used with the corresponding matchers (`testing-library/prefer-query-matchers`) + + + +The (DOM) Testing Library allows to query DOM elements using different types of queries such as `get*` and `query*`. Using `get*` throws an error in case the element is not found, while `query*` returns null instead of throwing (or empty array for `queryAllBy*` ones). + +It may be helpful to ensure that either `get*` or `query*` are always used for a given matcher. For example, `.toBeVisible()` and the negation `.not.toBeVisible()` both assume that an element exists in the DOM and will error if not. Using `get*` with `.toBeVisible()` ensures that if the element is not found the error thrown will offer better info than with `query*`. + +## Rule details + +This rule must be configured with a list of `validEntries`: for a given matcher, is `get*` or `query*` required. + +Assuming the following configuration: + +```json +{ + "testing-library/prefer-query-matchers": [ + 2, + { + "validEntries": [{ "matcher": "toBeVisible", "query": "get" }] + } + ] +} +``` + +Examples of **incorrect** code for this rule with the above configuration: + +```js +test('some test', () => { + render(); + + // use configured matcher with the disallowed `query*` + expect(screen.queryByText('button')).toBeVisible(); + expect(screen.queryByText('button')).not.toBeVisible(); + expect(screen.queryAllByText('button')[0]).toBeVisible(); + expect(screen.queryAllByText('button')[0]).not.toBeVisible(); +}); +``` + +Examples of **correct** code for this rule: + +```js +test('some test', async () => { + render(); + // use configured matcher with the allowed `get*` + expect(screen.getByText('button')).toBeVisible(); + expect(screen.getByText('button')).not.toBeVisible(); + expect(screen.getAllByText('button')[0]).toBeVisible(); + expect(screen.getAllByText('button')[0]).not.toBeVisible(); + + // use an unconfigured matcher with either `get* or `query* + expect(screen.getByText('button')).toBeEnabled(); + expect(screen.getAllByText('checkbox')[0]).not.toBeChecked(); + expect(screen.queryByText('button')).toHaveFocus(); + expect(screen.queryAllByText('button')[0]).not.toMatchMyCustomMatcher(); + + // `findBy*` queries are out of the scope for this rule + const button = await screen.findByText('submit'); + expect(button).toBeVisible(); +}); +``` + +## Options + +| Option | Required | Default | Details | +| -------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `validEntries` | No | `[]` | A list of objects with a `matcher` property (the name of any matcher, such as "toBeVisible") and a `query` property (either "get" or "query"). Indicates whether `get*` or `query*` are allowed with this matcher. | + +## Example + +```json +{ + "testing-library/prefer-query-matchers": [ + 2, + { + "validEntries": [{ "matcher": "toBeVisible", "query": "get" }] + } + ] +} +``` + +## Further Reading + +- [Testing Library queries cheatsheet](https://testing-library.com/docs/dom-testing-library/cheatsheet#queries) +- [jest-dom note about using `getBy` within assertions](https://testing-library.com/docs/ecosystem-jest-dom) diff --git a/docs/rules/prefer-screen-queries.md b/docs/rules/prefer-screen-queries.md index fa8efe52..0a7134f6 100644 --- a/docs/rules/prefer-screen-queries.md +++ b/docs/rules/prefer-screen-queries.md @@ -1,5 +1,9 @@ # Suggest using `screen` while querying (`testing-library/prefer-screen-queries`) +πŸ’Ό This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. + + + ## Rule Details DOM Testing Library (and other Testing Library frameworks built on top of it) exports a `screen` object which has every query (and a `debug` method). This works better with autocomplete and makes each test a little simpler to write and maintain. @@ -34,7 +38,7 @@ getByText('foo'); Examples of **correct** code for this rule: ```js -import { screen } from '@testing-library/any-framework'; +import { render, screen, within } from '@testing-library/any-framework'; // calling a query from the `screen` object render(); diff --git a/docs/rules/prefer-user-event.md b/docs/rules/prefer-user-event.md index f2e0210e..421a0875 100644 --- a/docs/rules/prefer-user-event.md +++ b/docs/rules/prefer-user-event.md @@ -1,4 +1,6 @@ -# Suggest using `userEvent` library instead of `fireEvent` for simulating user interaction (`testing-library/prefer-user-event`) +# Suggest using `userEvent` over `fireEvent` for simulating user interactions (`testing-library/prefer-user-event`) + + From [testing-library/dom-testing-library#107](https://github.com/testing-library/dom-testing-library/issues/107): @@ -68,17 +70,14 @@ This rule allows to exclude specific functions with an equivalent in `userEvent` The configuration consists of an array of strings with the names of fireEvents methods to be excluded. An example looks like this -```json -{ - "rules": { - "prefer-user-event": [ - "error", - { - "allowedMethods": ["click", "change"] - } - ] - } -} +```js +module.exports = { + rules: { + rules: { + 'prefer-user-event': ['error', { allowedMethods: ['click', 'change'] }], + }, + }, +}; ``` With this configuration example, the following use cases are considered valid diff --git a/docs/rules/prefer-wait-for.md b/docs/rules/prefer-wait-for.md deleted file mode 100644 index ee82769c..00000000 --- a/docs/rules/prefer-wait-for.md +++ /dev/null @@ -1,74 +0,0 @@ -# Use `waitFor` instead of deprecated wait methods (`testing-library/prefer-wait-for`) - -`dom-testing-library` v7 released a new async util called `waitFor` which satisfies the use cases of `wait`, `waitForElement`, and `waitForDomChange` making them deprecated. - -## Rule Details - -This rule aims to use `waitFor` async util rather than previous deprecated ones. - -Deprecated `wait` async utils are: - -- `wait` -- `waitForElement` -- `waitForDomChange` - -> This rule will auto fix deprecated async utils for you, including the necessary empty callback for `waitFor`. This means `wait();` will be replaced with `waitFor(() => {});` - -Examples of **incorrect** code for this rule: - -```js -import { wait, waitForElement, waitForDomChange } from '@testing-library/dom'; -// this also works for const { wait, waitForElement, waitForDomChange } = require ('@testing-library/dom') - -const foo = async () => { - await wait(); - await wait(() => {}); - await waitForElement(() => {}); - await waitForDomChange(); - await waitForDomChange(mutationObserverOptions); - await waitForDomChange({ timeout: 100 }); -}; - -import * as tl from '@testing-library/dom'; -// this also works for const tl = require('@testing-library/dom') -const foo = async () => { - await tl.wait(); - await tl.wait(() => {}); - await tl.waitForElement(() => {}); - await tl.waitForDomChange(); - await tl.waitForDomChange(mutationObserverOptions); - await tl.waitForDomChange({ timeout: 100 }); -}; -``` - -Examples of **correct** code for this rule: - -```js -import { waitFor, waitForElementToBeRemoved } from '@testing-library/dom'; -// this also works for const { waitFor, waitForElementToBeRemoved } = require('@testing-library/dom') -const foo = async () => { - // new waitFor method - await waitFor(() => {}); - - // previous waitForElementToBeRemoved is not deprecated - await waitForElementToBeRemoved(() => {}); -}; - -import * as tl from '@testing-library/dom'; -// this also works for const tl = require('@testing-library/dom') -const foo = async () => { - // new waitFor method - await tl.waitFor(() => {}); - - // previous waitForElementToBeRemoved is not deprecated - await tl.waitForElementToBeRemoved(() => {}); -}; -``` - -## When Not To Use It - -When using dom-testing-library (or any other Testing Library relying on dom-testing-library) prior to v7. - -## Further Reading - -- [dom-testing-library v7 release](https://github.com/testing-library/dom-testing-library/releases/tag/v7.0.0) diff --git a/docs/rules/render-result-naming-convention.md b/docs/rules/render-result-naming-convention.md index f9e2e4fe..acadab69 100644 --- a/docs/rules/render-result-naming-convention.md +++ b/docs/rules/render-result-naming-convention.md @@ -1,5 +1,9 @@ # Enforce a valid naming for return value from `render` (`testing-library/render-result-naming-convention`) +πŸ’Ό This rule is enabled in the following configs: `angular`, `marko`, `react`, `svelte`, `vue`. + + + > The name `wrapper` is old cruft from `enzyme` and we don't need that here. The return value from `render` is not "wrapping" anything. It's simply a collection of utilities that you should actually not often need anyway. ## Rule Details diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 00000000..1f4e5e7e --- /dev/null +++ b/index.d.ts @@ -0,0 +1,27 @@ +import type { Linter, Rule } from 'eslint'; + +declare const plugin: { + meta: { + name: string; + version: string; + }; + configs: { + angular: Linter.LegacyConfig; + dom: Linter.LegacyConfig; + marko: Linter.LegacyConfig; + react: Linter.LegacyConfig; + svelte: Linter.LegacyConfig; + vue: Linter.LegacyConfig; + 'flat/angular': Linter.FlatConfig; + 'flat/dom': Linter.FlatConfig; + 'flat/marko': Linter.FlatConfig; + 'flat/react': Linter.FlatConfig; + 'flat/svelte': Linter.FlatConfig; + 'flat/vue': Linter.FlatConfig; + }; + rules: { + [key: string]: Rule.RuleModule; + }; +}; + +export = plugin; diff --git a/jest.config.js b/jest.config.js index a595d520..f546d6ef 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,14 +1,14 @@ module.exports = { - testMatch: ['**/tests/**/*.test.ts'], - transform: { - '^.+\\.tsx?$': 'ts-jest', - }, - coverageThreshold: { - global: { - branches: 90, - functions: 90, - lines: 90, - statements: 90, - }, - }, + testMatch: ['**/tests/**/*.test.ts'], + transform: { + '^.+\\.ts$': '@swc/jest', + }, + coverageThreshold: { + global: { + branches: 90, + functions: 90, + lines: 90, + statements: 90, + }, + }, }; diff --git a/lib/configs/angular.ts b/lib/configs/angular.ts index af9b277c..ef0fcc22 100644 --- a/lib/configs/angular.ts +++ b/lib/configs/angular.ts @@ -1,27 +1,35 @@ // THIS CODE WAS AUTOMATICALLY GENERATED // DO NOT EDIT THIS CODE BY HAND -// YOU CAN REGENERATE IT USING npm run generate:configs +// YOU CAN REGENERATE IT USING pnpm 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-debugging-utils': '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-render-in-setup': 'error', - 'testing-library/no-wait-for-empty-callback': 'error', - 'testing-library/no-wait-for-multiple-assertions': 'error', - 'testing-library/no-wait-for-side-effects': 'error', - 'testing-library/no-wait-for-snapshot': 'error', - 'testing-library/prefer-find-by': 'error', - 'testing-library/prefer-presence-queries': 'error', - 'testing-library/prefer-query-by-disappearance': 'error', - 'testing-library/prefer-screen-queries': 'error', - 'testing-library/render-result-naming-convention': 'error', - }, + plugins: ['testing-library'], + rules: { + 'testing-library/await-async-events': [ + 'error', + { eventModule: 'userEvent' }, + ], + 'testing-library/await-async-queries': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/no-await-sync-events': [ + 'error', + { eventModules: ['fire-event'] }, + ], + 'testing-library/no-await-sync-queries': 'error', + 'testing-library/no-container': 'error', + 'testing-library/no-debugging-utils': 'warn', + 'testing-library/no-dom-import': ['error', 'angular'], + 'testing-library/no-global-regexp-flag-in-query': 'error', + 'testing-library/no-node-access': 'error', + 'testing-library/no-promise-in-fire-event': 'error', + 'testing-library/no-render-in-lifecycle': 'error', + 'testing-library/no-wait-for-multiple-assertions': 'error', + 'testing-library/no-wait-for-side-effects': 'error', + 'testing-library/no-wait-for-snapshot': 'error', + 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-presence-queries': 'error', + 'testing-library/prefer-query-by-disappearance': '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 index ce153733..98e8cfeb 100644 --- a/lib/configs/dom.ts +++ b/lib/configs/dom.ts @@ -1,21 +1,30 @@ // THIS CODE WAS AUTOMATICALLY GENERATED // DO NOT EDIT THIS CODE BY HAND -// YOU CAN REGENERATE IT USING npm run generate:configs +// YOU CAN REGENERATE IT USING pnpm 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/no-wait-for-multiple-assertions': 'error', - 'testing-library/no-wait-for-side-effects': 'error', - 'testing-library/no-wait-for-snapshot': 'error', - 'testing-library/prefer-find-by': 'error', - 'testing-library/prefer-presence-queries': 'error', - 'testing-library/prefer-query-by-disappearance': 'error', - 'testing-library/prefer-screen-queries': 'error', - }, + plugins: ['testing-library'], + rules: { + 'testing-library/await-async-events': [ + 'error', + { eventModule: 'userEvent' }, + ], + 'testing-library/await-async-queries': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/no-await-sync-events': [ + 'error', + { eventModules: ['fire-event'] }, + ], + 'testing-library/no-await-sync-queries': 'error', + 'testing-library/no-global-regexp-flag-in-query': 'error', + 'testing-library/no-node-access': 'error', + 'testing-library/no-promise-in-fire-event': 'error', + 'testing-library/no-wait-for-multiple-assertions': 'error', + 'testing-library/no-wait-for-side-effects': 'error', + 'testing-library/no-wait-for-snapshot': 'error', + 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-presence-queries': 'error', + 'testing-library/prefer-query-by-disappearance': 'error', + 'testing-library/prefer-screen-queries': 'error', + }, }; diff --git a/lib/configs/index.ts b/lib/configs/index.ts index 15b63782..eb441b29 100644 --- a/lib/configs/index.ts +++ b/lib/configs/index.ts @@ -1,24 +1,22 @@ import { join } from 'path'; -import type { TSESLint } from '@typescript-eslint/experimental-utils'; +import type { TSESLint } from '@typescript-eslint/utils'; import { - importDefault, - SUPPORTED_TESTING_FRAMEWORKS, - SupportedTestingFramework, + importDefault, + SUPPORTED_TESTING_FRAMEWORKS, + SupportedTestingFramework, } from '../utils'; -export type LinterConfigRules = Record; - const configsDir = __dirname; const getConfigForFramework = (framework: SupportedTestingFramework) => - importDefault(join(configsDir, framework)); + importDefault(join(configsDir, framework)); export default SUPPORTED_TESTING_FRAMEWORKS.reduce( - (allConfigs, framework) => ({ - ...allConfigs, - [framework]: getConfigForFramework(framework), - }), - {} -) as Record; + (allConfigs, framework) => ({ + ...allConfigs, + [framework]: getConfigForFramework(framework), + }), + {} +) as Record; diff --git a/lib/configs/marko.ts b/lib/configs/marko.ts new file mode 100644 index 00000000..1301d135 --- /dev/null +++ b/lib/configs/marko.ts @@ -0,0 +1,32 @@ +// THIS CODE WAS AUTOMATICALLY GENERATED +// DO NOT EDIT THIS CODE BY HAND +// YOU CAN REGENERATE IT USING pnpm run generate:configs + +export = { + plugins: ['testing-library'], + rules: { + 'testing-library/await-async-events': [ + 'error', + { eventModule: ['fireEvent', 'userEvent'] }, + ], + 'testing-library/await-async-queries': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/no-await-sync-queries': 'error', + 'testing-library/no-container': 'error', + 'testing-library/no-debugging-utils': 'warn', + 'testing-library/no-dom-import': ['error', 'marko'], + 'testing-library/no-global-regexp-flag-in-query': 'error', + 'testing-library/no-node-access': 'error', + 'testing-library/no-promise-in-fire-event': 'error', + 'testing-library/no-render-in-lifecycle': 'error', + 'testing-library/no-unnecessary-act': 'error', + 'testing-library/no-wait-for-multiple-assertions': 'error', + 'testing-library/no-wait-for-side-effects': 'error', + 'testing-library/no-wait-for-snapshot': 'error', + 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-presence-queries': 'error', + 'testing-library/prefer-query-by-disappearance': 'error', + 'testing-library/prefer-screen-queries': 'error', + 'testing-library/render-result-naming-convention': 'error', + }, +}; diff --git a/lib/configs/react.ts b/lib/configs/react.ts index 5e2bc966..c1e293b3 100644 --- a/lib/configs/react.ts +++ b/lib/configs/react.ts @@ -1,28 +1,37 @@ // THIS CODE WAS AUTOMATICALLY GENERATED // DO NOT EDIT THIS CODE BY HAND -// YOU CAN REGENERATE IT USING npm run generate:configs +// YOU CAN REGENERATE IT USING pnpm 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-debugging-utils': '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-render-in-setup': 'error', - 'testing-library/no-unnecessary-act': 'error', - 'testing-library/no-wait-for-empty-callback': 'error', - 'testing-library/no-wait-for-multiple-assertions': 'error', - 'testing-library/no-wait-for-side-effects': 'error', - 'testing-library/no-wait-for-snapshot': 'error', - 'testing-library/prefer-find-by': 'error', - 'testing-library/prefer-presence-queries': 'error', - 'testing-library/prefer-query-by-disappearance': 'error', - 'testing-library/prefer-screen-queries': 'error', - 'testing-library/render-result-naming-convention': 'error', - }, + plugins: ['testing-library'], + rules: { + 'testing-library/await-async-events': [ + 'error', + { eventModule: 'userEvent' }, + ], + 'testing-library/await-async-queries': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/no-await-sync-events': [ + 'error', + { eventModules: ['fire-event'] }, + ], + 'testing-library/no-await-sync-queries': 'error', + 'testing-library/no-container': 'error', + 'testing-library/no-debugging-utils': 'warn', + 'testing-library/no-dom-import': ['error', 'react'], + 'testing-library/no-global-regexp-flag-in-query': 'error', + 'testing-library/no-manual-cleanup': 'error', + 'testing-library/no-node-access': 'error', + 'testing-library/no-promise-in-fire-event': 'error', + 'testing-library/no-render-in-lifecycle': 'error', + 'testing-library/no-unnecessary-act': 'error', + 'testing-library/no-wait-for-multiple-assertions': 'error', + 'testing-library/no-wait-for-side-effects': 'error', + 'testing-library/no-wait-for-snapshot': 'error', + 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-presence-queries': 'error', + 'testing-library/prefer-query-by-disappearance': 'error', + 'testing-library/prefer-screen-queries': 'error', + 'testing-library/render-result-naming-convention': 'error', + }, }; diff --git a/lib/configs/svelte.ts b/lib/configs/svelte.ts new file mode 100644 index 00000000..7713f9db --- /dev/null +++ b/lib/configs/svelte.ts @@ -0,0 +1,32 @@ +// THIS CODE WAS AUTOMATICALLY GENERATED +// DO NOT EDIT THIS CODE BY HAND +// YOU CAN REGENERATE IT USING pnpm run generate:configs + +export = { + plugins: ['testing-library'], + rules: { + 'testing-library/await-async-events': [ + 'error', + { eventModule: ['fireEvent', 'userEvent'] }, + ], + 'testing-library/await-async-queries': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/no-await-sync-queries': 'error', + 'testing-library/no-container': 'error', + 'testing-library/no-debugging-utils': 'warn', + 'testing-library/no-dom-import': ['error', 'svelte'], + 'testing-library/no-global-regexp-flag-in-query': 'error', + 'testing-library/no-manual-cleanup': 'error', + 'testing-library/no-node-access': 'error', + 'testing-library/no-promise-in-fire-event': 'error', + 'testing-library/no-render-in-lifecycle': 'error', + 'testing-library/no-wait-for-multiple-assertions': 'error', + 'testing-library/no-wait-for-side-effects': 'error', + 'testing-library/no-wait-for-snapshot': 'error', + 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-presence-queries': 'error', + 'testing-library/prefer-query-by-disappearance': '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 index cf9e42bb..f7fd1487 100644 --- a/lib/configs/vue.ts +++ b/lib/configs/vue.ts @@ -1,28 +1,32 @@ // THIS CODE WAS AUTOMATICALLY GENERATED // DO NOT EDIT THIS CODE BY HAND -// YOU CAN REGENERATE IT USING npm run generate:configs +// YOU CAN REGENERATE IT USING pnpm 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-debugging-utils': '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-render-in-setup': 'error', - 'testing-library/no-wait-for-empty-callback': 'error', - 'testing-library/no-wait-for-multiple-assertions': 'error', - 'testing-library/no-wait-for-side-effects': 'error', - 'testing-library/no-wait-for-snapshot': 'error', - 'testing-library/prefer-find-by': 'error', - 'testing-library/prefer-presence-queries': 'error', - 'testing-library/prefer-query-by-disappearance': 'error', - 'testing-library/prefer-screen-queries': 'error', - 'testing-library/render-result-naming-convention': 'error', - }, + plugins: ['testing-library'], + rules: { + 'testing-library/await-async-events': [ + 'error', + { eventModule: ['fireEvent', 'userEvent'] }, + ], + 'testing-library/await-async-queries': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/no-await-sync-queries': 'error', + 'testing-library/no-container': 'error', + 'testing-library/no-debugging-utils': 'warn', + 'testing-library/no-dom-import': ['error', 'vue'], + 'testing-library/no-global-regexp-flag-in-query': 'error', + 'testing-library/no-manual-cleanup': 'error', + 'testing-library/no-node-access': 'error', + 'testing-library/no-promise-in-fire-event': 'error', + 'testing-library/no-render-in-lifecycle': 'error', + 'testing-library/no-wait-for-multiple-assertions': 'error', + 'testing-library/no-wait-for-side-effects': 'error', + 'testing-library/no-wait-for-snapshot': 'error', + 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-presence-queries': 'error', + 'testing-library/prefer-query-by-disappearance': 'error', + 'testing-library/prefer-screen-queries': 'error', + 'testing-library/render-result-naming-convention': 'error', + }, }; diff --git a/lib/create-testing-library-rule/detect-testing-library-utils.ts b/lib/create-testing-library-rule/detect-testing-library-utils.ts index f811dec7..fd5a4973 100644 --- a/lib/create-testing-library-rule/detect-testing-library-utils.ts +++ b/lib/create-testing-library-rule/detect-testing-library-utils.ts @@ -1,63 +1,67 @@ -import { - ASTUtils, - TSESLint, - TSESTree, -} from '@typescript-eslint/experimental-utils'; +import { ASTUtils, TSESLint, TSESTree } from '@typescript-eslint/utils'; import { - findClosestVariableDeclaratorNode, - findImportSpecifier, - getAssertNodeInfo, - getDeepestIdentifierNode, - getImportModuleName, - getPropertyIdentifierNode, - getReferenceNode, - hasImportMatch, - ImportModuleNode, - isCallExpression, - isImportDeclaration, - isImportDefaultSpecifier, - isImportSpecifier, - isLiteral, - isMemberExpression, + findClosestVariableDeclaratorNode, + findImportSpecifier, + getAssertNodeInfo, + getDeepestIdentifierNode, + getImportModuleName, + getPropertyIdentifierNode, + getReferenceNode, + hasImportMatch, + ImportModuleNode, + isCallExpression, + isImportDeclaration, + isImportDefaultSpecifier, + isImportSpecifier, + isLiteral, + isMemberExpression, } from '../node-utils'; import { - ABSENCE_MATCHERS, - ALL_QUERIES_COMBINATIONS, - ASYNC_UTILS, - DEBUG_UTILS, - PRESENCE_MATCHERS, + ABSENCE_MATCHERS, + ALL_QUERIES_COMBINATIONS, + ASYNC_UTILS, + DEBUG_UTILS, + PRESENCE_MATCHERS, + USER_EVENT_MODULE, } from '../utils'; +import { + isCustomTestingLibraryModule, + isOfficialTestingLibraryModule, + isTestingLibraryModule, +} from '../utils/is-testing-library-module'; -const SETTING_OPTION_OFF = 'off' as const; +const SETTING_OPTION_OFF = 'off'; export type TestingLibrarySettings = { - 'testing-library/utils-module'?: string | typeof SETTING_OPTION_OFF; - 'testing-library/custom-renders'?: string[] | typeof SETTING_OPTION_OFF; - 'testing-library/custom-queries'?: string[] | typeof SETTING_OPTION_OFF; + 'testing-library/utils-module'?: + | typeof SETTING_OPTION_OFF + | (string & NonNullable); + 'testing-library/custom-renders'?: string[] | typeof SETTING_OPTION_OFF; + 'testing-library/custom-queries'?: string[] | typeof SETTING_OPTION_OFF; }; export type TestingLibraryContext< - TOptions extends readonly unknown[], - TMessageIds extends string + TMessageIds extends string, + TOptions extends readonly unknown[], > = Readonly< - TSESLint.RuleContext & { - settings: TestingLibrarySettings; - } + TSESLint.RuleContext & { + settings: TestingLibrarySettings; + } >; export type EnhancedRuleCreate< - TOptions extends readonly unknown[], - TMessageIds extends string, - TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener + TMessageIds extends string, + TOptions extends readonly unknown[], > = ( - context: TestingLibraryContext, - optionsWithDefault: Readonly, - detectionHelpers: Readonly -) => TRuleListener; + context: TestingLibraryContext, + optionsWithDefault: Readonly, + detectionHelpers: Readonly +) => TSESLint.RuleListener; // Helpers methods type GetTestingLibraryImportNodeFn = () => ImportModuleNode | null; +type GetTestingLibraryImportNodesFn = () => ImportModuleNode[]; type GetCustomModuleImportNodeFn = () => ImportModuleNode | null; type GetTestingLibraryImportNameFn = () => string | undefined; type GetCustomModuleImportNameFn = () => string | undefined; @@ -71,65 +75,70 @@ type IsQueryFn = (node: TSESTree.Identifier) => boolean; type IsCustomQueryFn = (node: TSESTree.Identifier) => boolean; type IsBuiltInQueryFn = (node: TSESTree.Identifier) => boolean; type IsAsyncUtilFn = ( - node: TSESTree.Identifier, - validNames?: readonly typeof ASYNC_UTILS[number][] + node: TSESTree.Identifier, + validNames?: readonly (typeof ASYNC_UTILS)[number][] ) => boolean; type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean; type IsUserEventMethodFn = (node: TSESTree.Identifier) => boolean; type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean; type IsCreateEventUtil = ( - node: TSESTree.CallExpression | TSESTree.Identifier + node: TSESTree.CallExpression | TSESTree.Identifier ) => boolean; type IsRenderVariableDeclaratorFn = ( - node: TSESTree.VariableDeclarator + node: TSESTree.VariableDeclarator ) => boolean; type IsDebugUtilFn = ( - identifierNode: TSESTree.Identifier, - validNames?: ReadonlyArray + identifierNode: TSESTree.Identifier, + validNames?: ReadonlyArray<(typeof DEBUG_UTILS)[number]> ) => boolean; type IsPresenceAssertFn = (node: TSESTree.MemberExpression) => boolean; +type IsMatchingAssertFn = ( + node: TSESTree.MemberExpression, + matcherName: string +) => boolean; type IsAbsenceAssertFn = (node: TSESTree.MemberExpression) => boolean; type CanReportErrorsFn = () => boolean; type FindImportedTestingLibraryUtilSpecifierFn = ( - specifierName: string + specifierName: string ) => TSESTree.Identifier | TSESTree.ImportClause | undefined; type IsNodeComingFromTestingLibraryFn = ( - node: TSESTree.Identifier | TSESTree.MemberExpression + node: TSESTree.Identifier | TSESTree.MemberExpression ) => boolean; export interface DetectionHelpers { - getTestingLibraryImportNode: GetTestingLibraryImportNodeFn; - getCustomModuleImportNode: GetCustomModuleImportNodeFn; - getTestingLibraryImportName: GetTestingLibraryImportNameFn; - getCustomModuleImportName: GetCustomModuleImportNameFn; - isTestingLibraryImported: IsTestingLibraryImportedFn; - isTestingLibraryUtil: (node: TSESTree.Identifier) => boolean; - isGetQueryVariant: IsGetQueryVariantFn; - isQueryQueryVariant: IsQueryQueryVariantFn; - isFindQueryVariant: IsFindQueryVariantFn; - isSyncQuery: IsSyncQueryFn; - isAsyncQuery: IsAsyncQueryFn; - isQuery: IsQueryFn; - isCustomQuery: IsCustomQueryFn; - isBuiltInQuery: IsBuiltInQueryFn; - isAsyncUtil: IsAsyncUtilFn; - isFireEventUtil: (node: TSESTree.Identifier) => boolean; - isUserEventUtil: (node: TSESTree.Identifier) => boolean; - isFireEventMethod: IsFireEventMethodFn; - isUserEventMethod: IsUserEventMethodFn; - isRenderUtil: IsRenderUtilFn; - isCreateEventUtil: IsCreateEventUtil; - isRenderVariableDeclarator: IsRenderVariableDeclaratorFn; - isDebugUtil: IsDebugUtilFn; - isActUtil: (node: TSESTree.Identifier) => boolean; - isPresenceAssert: IsPresenceAssertFn; - isAbsenceAssert: IsAbsenceAssertFn; - canReportErrors: CanReportErrorsFn; - findImportedTestingLibraryUtilSpecifier: FindImportedTestingLibraryUtilSpecifierFn; - isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn; + getTestingLibraryImportNode: GetTestingLibraryImportNodeFn; + getAllTestingLibraryImportNodes: GetTestingLibraryImportNodesFn; + getCustomModuleImportNode: GetCustomModuleImportNodeFn; + getTestingLibraryImportName: GetTestingLibraryImportNameFn; + getCustomModuleImportName: GetCustomModuleImportNameFn; + isTestingLibraryImported: IsTestingLibraryImportedFn; + isTestingLibraryUtil: (node: TSESTree.Identifier) => boolean; + isGetQueryVariant: IsGetQueryVariantFn; + isQueryQueryVariant: IsQueryQueryVariantFn; + isFindQueryVariant: IsFindQueryVariantFn; + isSyncQuery: IsSyncQueryFn; + isAsyncQuery: IsAsyncQueryFn; + isQuery: IsQueryFn; + isCustomQuery: IsCustomQueryFn; + isBuiltInQuery: IsBuiltInQueryFn; + isAsyncUtil: IsAsyncUtilFn; + isFireEventUtil: (node: TSESTree.Identifier) => boolean; + isUserEventUtil: (node: TSESTree.Identifier) => boolean; + isFireEventMethod: IsFireEventMethodFn; + isUserEventMethod: IsUserEventMethodFn; + isRenderUtil: IsRenderUtilFn; + isCreateEventUtil: IsCreateEventUtil; + isRenderVariableDeclarator: IsRenderVariableDeclaratorFn; + isDebugUtil: IsDebugUtilFn; + isActUtil: (node: TSESTree.Identifier) => boolean; + isPresenceAssert: IsPresenceAssertFn; + isAbsenceAssert: IsAbsenceAssertFn; + isMatchingAssert: IsMatchingAssertFn; + canReportErrors: CanReportErrorsFn; + findImportedTestingLibraryUtilSpecifier: FindImportedTestingLibraryUtilSpecifierFn; + isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn; } -const USER_EVENT_PACKAGE = '@testing-library/user-event'; const REACT_DOM_TEST_UTILS_PACKAGE = 'react-dom/test-utils'; const FIRE_EVENT_NAME = 'fireEvent'; const CREATE_EVENT_NAME = 'createEvent'; @@ -137,983 +146,995 @@ const USER_EVENT_NAME = 'userEvent'; const RENDER_NAME = 'render'; export type DetectionOptions = { - /** - * If true, force `detectTestingLibraryUtils` to skip `canReportErrors` - * so it doesn't opt-out rule listener. - * - * Useful when some rule apply to files other than testing ones - * (e.g. `consistent-data-testid`) - */ - skipRuleReportingCheck: boolean; + /** + * If true, force `detectTestingLibraryUtils` to skip `canReportErrors` + * so it doesn't opt-out rule listener. + * + * Useful when some rule apply to files other than testing ones + * (e.g. `consistent-data-testid`) + */ + skipRuleReportingCheck: boolean; }; /** * Enhances a given rule `create` with helpers to detect Testing Library utils. */ export function detectTestingLibraryUtils< - TOptions extends readonly unknown[], - TMessageIds extends string, - TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener + TMessageIds extends string, + TOptions extends readonly unknown[], >( - ruleCreate: EnhancedRuleCreate, - { skipRuleReportingCheck = false }: Partial = {} + ruleCreate: EnhancedRuleCreate, + { skipRuleReportingCheck = false }: Partial = {} ) { - return ( - context: TestingLibraryContext, - optionsWithDefault: Readonly - ): TSESLint.RuleListener => { - let importedTestingLibraryNode: ImportModuleNode | null = null; - let importedCustomModuleNode: ImportModuleNode | null = null; - let importedUserEventLibraryNode: ImportModuleNode | null = null; - let importedReactDomTestUtilsNode: ImportModuleNode | null = null; - - // Init options based on shared ESLint settings - const customModuleSetting = - context.settings['testing-library/utils-module']; - const customRendersSetting = - context.settings['testing-library/custom-renders']; - const customQueriesSetting = - context.settings['testing-library/custom-queries']; - - /** - * Small method to extract common checks to determine whether a node is - * related to Testing Library or not. - * - * To determine whether a node is a valid Testing Library util, there are - * two conditions to match: - * - it's named in a particular way (decided by given callback) - * - it's imported from valid Testing Library module (depends on aggressive - * reporting) - */ - function isPotentialTestingLibraryFunction( - node: TSESTree.Identifier | null | undefined, - isPotentialFunctionCallback: ( - identifierNodeName: string, - originalNodeName?: string - ) => boolean - ): boolean { - if (!node) { - return false; - } - - const referenceNode = getReferenceNode(node); - const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode); - - if (!referenceNodeIdentifier) { - return false; - } - - const importedUtilSpecifier = getTestingLibraryImportedUtilSpecifier( - referenceNodeIdentifier - ); - - const originalNodeName = - isImportSpecifier(importedUtilSpecifier) && - importedUtilSpecifier.local.name !== importedUtilSpecifier.imported.name - ? importedUtilSpecifier.imported.name - : undefined; - - if (!isPotentialFunctionCallback(node.name, originalNodeName)) { - return false; - } - - if (isAggressiveModuleReportingEnabled()) { - return true; - } - - return isNodeComingFromTestingLibrary(referenceNodeIdentifier); - } - - /** - * Determines whether aggressive module reporting is enabled or not. - * - * This aggressive reporting mechanism is considered as enabled when custom - * module is not set, so we need to assume everything matching Testing - * Library utils is related to Testing Library no matter from where module - * they are coming from. Otherwise, this aggressive reporting mechanism is - * opted-out in favour to report only those utils coming from Testing - * Library package or custom module set up on settings. - */ - const isAggressiveModuleReportingEnabled = () => !customModuleSetting; - - /** - * Determines whether aggressive render reporting is enabled or not. - * - * This aggressive reporting mechanism is considered as enabled when custom - * renders are not set, so we need to assume every method containing - * "render" is a valid Testing Library `render`. Otherwise, this aggressive - * reporting mechanism is opted-out in favour to report only `render` or - * names set up on custom renders setting. - */ - const isAggressiveRenderReportingEnabled = (): boolean => { - const isSwitchedOff = customRendersSetting === SETTING_OPTION_OFF; - const hasCustomOptions = - Array.isArray(customRendersSetting) && customRendersSetting.length > 0; - - return !isSwitchedOff && !hasCustomOptions; - }; - - /** - * Determines whether Aggressive Reporting for queries is enabled or not. - * - * This Aggressive Reporting mechanism is considered as enabled when custom-queries setting is not set, - * so the plugin needs to report both built-in and custom queries. - * Otherwise, this Aggressive Reporting mechanism is opted-out in favour of reporting only built-in queries + those - * indicated in custom-queries setting. - */ - const isAggressiveQueryReportingEnabled = (): boolean => { - const isSwitchedOff = customQueriesSetting === SETTING_OPTION_OFF; - const hasCustomOptions = - Array.isArray(customQueriesSetting) && customQueriesSetting.length > 0; - - return !isSwitchedOff && !hasCustomOptions; - }; - - const getCustomModule = (): string | undefined => { - if ( - !isAggressiveModuleReportingEnabled() && - customModuleSetting !== SETTING_OPTION_OFF - ) { - return customModuleSetting; - } - return undefined; - }; - - const getCustomRenders = (): string[] => { - if ( - !isAggressiveRenderReportingEnabled() && - customRendersSetting !== SETTING_OPTION_OFF - ) { - return customRendersSetting as string[]; - } - - return []; - }; - - const getCustomQueries = (): string[] => { - if ( - !isAggressiveQueryReportingEnabled() && - customQueriesSetting !== SETTING_OPTION_OFF - ) { - return customQueriesSetting as string[]; - } - - return []; - }; - - // Helpers for Testing Library detection. - const getTestingLibraryImportNode: GetTestingLibraryImportNodeFn = () => { - return importedTestingLibraryNode; - }; - - const getCustomModuleImportNode: GetCustomModuleImportNodeFn = () => { - return importedCustomModuleNode; - }; - - const getTestingLibraryImportName: GetTestingLibraryImportNameFn = () => { - return getImportModuleName(importedTestingLibraryNode); - }; - - const getCustomModuleImportName: GetCustomModuleImportNameFn = () => { - return getImportModuleName(importedCustomModuleNode); - }; - - /** - * Determines whether Testing Library utils are imported or not for - * current file being analyzed. - * - * By default, it is ALWAYS considered as imported. This is what we call - * "aggressive reporting" so we don't miss TL utils reexported from - * custom modules. - * - * However, there is a setting to customize the module where TL utils can - * be imported from: "testing-library/utils-module". If this setting is enabled, - * then this method will return `true` ONLY IF a testing-library package - * or custom module are imported. - */ - const isTestingLibraryImported: IsTestingLibraryImportedFn = ( - isStrict = false - ) => { - const isSomeModuleImported = - !!importedTestingLibraryNode || !!importedCustomModuleNode; - - return ( - (!isStrict && isAggressiveModuleReportingEnabled()) || - isSomeModuleImported - ); - }; - - /** - * Determines whether a given node is a reportable query, - * either a built-in or a custom one. - * - * Depending on Aggressive Query Reporting setting, custom queries will be - * reportable or not. - */ - const isQuery: IsQueryFn = (node) => { - const hasQueryPattern = /^(get|query|find)(All)?By.+$/.test(node.name); - if (!hasQueryPattern) { - return false; - } - - if (isAggressiveQueryReportingEnabled()) { - return true; - } - - const customQueries = getCustomQueries(); - const isBuiltInQuery = ALL_QUERIES_COMBINATIONS.includes(node.name); - const isReportableCustomQuery = customQueries.some((pattern) => - new RegExp(pattern).test(node.name) - ); - return isBuiltInQuery || isReportableCustomQuery; - }; - - /** - * Determines whether a given node is `get*` query variant or not. - */ - const isGetQueryVariant: IsGetQueryVariantFn = (node) => { - return isQuery(node) && node.name.startsWith('get'); - }; - - /** - * Determines whether a given node is `query*` query variant or not. - */ - const isQueryQueryVariant: IsQueryQueryVariantFn = (node) => { - return isQuery(node) && node.name.startsWith('query'); - }; - - /** - * Determines whether a given node is `find*` query variant or not. - */ - const isFindQueryVariant: IsFindQueryVariantFn = (node) => { - return isQuery(node) && node.name.startsWith('find'); - }; - - /** - * Determines whether a given node is sync query or not. - */ - const isSyncQuery: IsSyncQueryFn = (node) => { - return isGetQueryVariant(node) || isQueryQueryVariant(node); - }; - - /** - * Determines whether a given node is async query or not. - */ - const isAsyncQuery: IsAsyncQueryFn = (node) => { - return isFindQueryVariant(node); - }; - - const isCustomQuery: IsCustomQueryFn = (node) => { - return isQuery(node) && !ALL_QUERIES_COMBINATIONS.includes(node.name); - }; - - const isBuiltInQuery = (node: TSESTree.Identifier): boolean => { - return isQuery(node) && ALL_QUERIES_COMBINATIONS.includes(node.name); - }; - - /** - * Determines whether a given node is a valid async util or not. - * - * A node will be interpreted as a valid async util based on two conditions: - * the name matches with some Testing Library async util, and the node is - * coming from Testing Library module. - * - * The latter depends on Aggressive module reporting: - * if enabled, then it doesn't matter from where the given node was imported - * from as it will be considered part of Testing Library. - * Otherwise, it means `custom-module` has been set up, so only those nodes - * coming from Testing Library will be considered as valid. - */ - const isAsyncUtil: IsAsyncUtilFn = (node, validNames = ASYNC_UTILS) => { - return isPotentialTestingLibraryFunction( - node, - (identifierNodeName, originalNodeName) => { - return ( - (validNames as string[]).includes(identifierNodeName) || - (!!originalNodeName && - (validNames as string[]).includes(originalNodeName)) - ); - } - ); - }; - - /** - * Determines whether a given node is fireEvent util itself or not. - * - * Not to be confused with {@link isFireEventMethod} - */ - const isFireEventUtil = (node: TSESTree.Identifier): boolean => { - return isPotentialTestingLibraryFunction( - node, - (identifierNodeName, originalNodeName) => { - return [identifierNodeName, originalNodeName].includes('fireEvent'); - } - ); - }; - - /** - * Determines whether a given node is userEvent util itself or not. - * - * Not to be confused with {@link isUserEventMethod} - */ - const isUserEventUtil = (node: TSESTree.Identifier): boolean => { - const userEvent = findImportedUserEventSpecifier(); - let userEventName: string | undefined; - - if (userEvent) { - userEventName = userEvent.name; - } else if (isAggressiveModuleReportingEnabled()) { - userEventName = USER_EVENT_NAME; - } - - if (!userEventName) { - return false; - } - - return node.name === userEventName; - }; - - /** - * Determines whether a given node is fireEvent method or not - */ - // eslint-disable-next-line complexity - const isFireEventMethod: IsFireEventMethodFn = (node) => { - const fireEventUtil = - findImportedTestingLibraryUtilSpecifier(FIRE_EVENT_NAME); - let fireEventUtilName: string | undefined; - - if (fireEventUtil) { - fireEventUtilName = ASTUtils.isIdentifier(fireEventUtil) - ? fireEventUtil.name - : fireEventUtil.local.name; - } else if (isAggressiveModuleReportingEnabled()) { - fireEventUtilName = FIRE_EVENT_NAME; - } - - if (!fireEventUtilName) { - return false; - } - - const parentMemberExpression: TSESTree.MemberExpression | undefined = - node.parent && isMemberExpression(node.parent) - ? node.parent - : undefined; - - const parentCallExpression: TSESTree.CallExpression | undefined = - node.parent && isCallExpression(node.parent) ? node.parent : undefined; - - if (!parentMemberExpression && !parentCallExpression) { - return false; - } - - // check fireEvent('method', node) usage - if (parentCallExpression) { - return [fireEventUtilName, FIRE_EVENT_NAME].includes(node.name); - } - - // we know it's defined at this point, but TS seems to think it is not - // so here I'm enforcing it once in order to avoid using "!" operator every time - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const definedParentMemberExpression = parentMemberExpression!; - - // check fireEvent.click() usage - const regularCall = - ASTUtils.isIdentifier(definedParentMemberExpression.object) && - isCallExpression(definedParentMemberExpression.parent) && - definedParentMemberExpression.object.name === fireEventUtilName && - node.name !== FIRE_EVENT_NAME && - node.name !== fireEventUtilName; - - // check testingLibraryUtils.fireEvent.click() usage - const wildcardCall = - isMemberExpression(definedParentMemberExpression.object) && - ASTUtils.isIdentifier(definedParentMemberExpression.object.object) && - definedParentMemberExpression.object.object.name === - fireEventUtilName && - ASTUtils.isIdentifier(definedParentMemberExpression.object.property) && - definedParentMemberExpression.object.property.name === - FIRE_EVENT_NAME && - node.name !== FIRE_EVENT_NAME && - node.name !== fireEventUtilName; - - // check testingLibraryUtils.fireEvent('click') - const wildcardCallWithCallExpression = - ASTUtils.isIdentifier(definedParentMemberExpression.object) && - definedParentMemberExpression.object.name === fireEventUtilName && - ASTUtils.isIdentifier(definedParentMemberExpression.property) && - definedParentMemberExpression.property.name === FIRE_EVENT_NAME && - !isMemberExpression(definedParentMemberExpression.parent) && - node.name === FIRE_EVENT_NAME && - node.name !== fireEventUtilName; - - return regularCall || wildcardCall || wildcardCallWithCallExpression; - }; - - const isUserEventMethod: IsUserEventMethodFn = (node) => { - const userEvent = findImportedUserEventSpecifier(); - let userEventName: string | undefined; - - if (userEvent) { - userEventName = userEvent.name; - } else if (isAggressiveModuleReportingEnabled()) { - userEventName = USER_EVENT_NAME; - } - - if (!userEventName) { - return false; - } - - const parentMemberExpression: TSESTree.MemberExpression | undefined = - node.parent && isMemberExpression(node.parent) - ? node.parent - : undefined; - - if (!parentMemberExpression) { - return false; - } - - // make sure that given node it's not userEvent object itself - if ( - [userEventName, USER_EVENT_NAME].includes(node.name) || - (ASTUtils.isIdentifier(parentMemberExpression.object) && - parentMemberExpression.object.name === node.name) - ) { - return false; - } - - // check userEvent.click() usage - return ( - ASTUtils.isIdentifier(parentMemberExpression.object) && - parentMemberExpression.object.name === userEventName - ); - }; - - /** - * Determines whether a given node is a valid render util or not. - * - * A node will be interpreted as a valid render based on two conditions: - * the name matches with a valid "render" option, and the node is coming - * from Testing Library module. This depends on: - * - * - Aggressive render reporting: if enabled, then every node name - * containing "render" will be assumed as Testing Library render util. - * Otherwise, it means `custom-modules` has been set up, so only those nodes - * named as "render" or some of the `custom-modules` options will be - * considered as Testing Library render util. - * - Aggressive module reporting: if enabled, then it doesn't matter from - * where the given node was imported from as it will be considered part of - * Testing Library. Otherwise, it means `custom-module` has been set up, so - * only those nodes coming from Testing Library will be considered as valid. - */ - const isRenderUtil: IsRenderUtilFn = (node) => - isPotentialTestingLibraryFunction( - node, - (identifierNodeName, originalNodeName) => { - if (isAggressiveRenderReportingEnabled()) { - return identifierNodeName.toLowerCase().includes(RENDER_NAME); - } - - return [RENDER_NAME, ...getCustomRenders()].some( - (validRenderName) => - validRenderName === identifierNodeName || - (Boolean(originalNodeName) && - validRenderName === originalNodeName) - ); - } - ); - - const isCreateEventUtil: IsCreateEventUtil = (node) => { - const isCreateEventCallback = ( - identifierNodeName: string, - originalNodeName?: string - ) => [identifierNodeName, originalNodeName].includes(CREATE_EVENT_NAME); - if ( - isCallExpression(node) && - isMemberExpression(node.callee) && - ASTUtils.isIdentifier(node.callee.object) - ) { - return isPotentialTestingLibraryFunction( - node.callee.object, - isCreateEventCallback - ); - } - - if ( - isCallExpression(node) && - isMemberExpression(node.callee) && - isMemberExpression(node.callee.object) && - ASTUtils.isIdentifier(node.callee.object.property) - ) { - return isPotentialTestingLibraryFunction( - node.callee.object.property, - isCreateEventCallback - ); - } - const identifier = getDeepestIdentifierNode(node); - return isPotentialTestingLibraryFunction( - identifier, - isCreateEventCallback - ); - }; - - const isRenderVariableDeclarator: IsRenderVariableDeclaratorFn = (node) => { - if (!node.init) { - return false; - } - const initIdentifierNode = getDeepestIdentifierNode(node.init); - - if (!initIdentifierNode) { - return false; - } - - return isRenderUtil(initIdentifierNode); - }; - - const isDebugUtil: IsDebugUtilFn = ( - identifierNode, - validNames = DEBUG_UTILS - ) => { - const isBuiltInConsole = - isMemberExpression(identifierNode.parent) && - ASTUtils.isIdentifier(identifierNode.parent.object) && - identifierNode.parent.object.name === 'console'; - - return ( - !isBuiltInConsole && - isPotentialTestingLibraryFunction( - identifierNode, - (identifierNodeName, originalNodeName) => { - return ( - (validNames as string[]).includes(identifierNodeName) || - (!!originalNodeName && - (validNames as string[]).includes(originalNodeName)) - ); - } - ) - ); - }; - - /** - * Determines whether a given node is some reportable `act` util. - * - * An `act` is reportable if some of these conditions is met: - * - it's related to Testing Library module (this depends on Aggressive Reporting) - * - it's related to React DOM Test Utils - */ - const isActUtil = (node: TSESTree.Identifier): boolean => { - const isTestingLibraryAct = isPotentialTestingLibraryFunction( - node, - (identifierNodeName, originalNodeName) => { - return [identifierNodeName, originalNodeName] - .filter(Boolean) - .includes('act'); - } - ); - - const isReactDomTestUtilsAct = (() => { - if (!importedReactDomTestUtilsNode) { - return false; - } - const referenceNode = getReferenceNode(node); - const referenceNodeIdentifier = - getPropertyIdentifierNode(referenceNode); - if (!referenceNodeIdentifier) { - return false; - } - - const importedUtilSpecifier = findImportSpecifier( - node.name, - importedReactDomTestUtilsNode - ); - if (!importedUtilSpecifier) { - return false; - } - - const importDeclaration = (() => { - if (isImportDeclaration(importedUtilSpecifier.parent)) { - return importedUtilSpecifier.parent; - } - - const variableDeclarator = findClosestVariableDeclaratorNode( - importedUtilSpecifier - ); - - if (isCallExpression(variableDeclarator?.init)) { - return variableDeclarator?.init; - } - - return undefined; - })(); - if (!importDeclaration) { - return false; - } - - const importDeclarationName = getImportModuleName(importDeclaration); - if (!importDeclarationName) { - return false; - } - - if (importDeclarationName !== REACT_DOM_TEST_UTILS_PACKAGE) { - return false; - } - - return hasImportMatch( - importedUtilSpecifier, - referenceNodeIdentifier.name - ); - })(); - - return isTestingLibraryAct || isReactDomTestUtilsAct; - }; - - const isTestingLibraryUtil = (node: TSESTree.Identifier): boolean => { - return ( - isAsyncUtil(node) || - isQuery(node) || - isRenderUtil(node) || - isFireEventMethod(node) || - isUserEventMethod(node) || - isActUtil(node) || - isCreateEventUtil(node) - ); - }; - - /** - * Determines whether a given MemberExpression node is a presence assert - * - * Presence asserts could have shape of: - * - expect(element).toBeInTheDocument() - * - expect(element).not.toBeNull() - */ - const isPresenceAssert: IsPresenceAssertFn = (node) => { - const { matcher, isNegated } = getAssertNodeInfo(node); - - if (!matcher) { - return false; - } - - return isNegated - ? ABSENCE_MATCHERS.includes(matcher) - : PRESENCE_MATCHERS.includes(matcher); - }; - - /** - * Determines whether a given MemberExpression node is an absence assert - * - * Absence asserts could have shape of: - * - expect(element).toBeNull() - * - expect(element).not.toBeInTheDocument() - */ - const isAbsenceAssert: IsAbsenceAssertFn = (node) => { - const { matcher, isNegated } = getAssertNodeInfo(node); - - if (!matcher) { - return false; - } - - return isNegated - ? PRESENCE_MATCHERS.includes(matcher) - : ABSENCE_MATCHERS.includes(matcher); - }; - - /** - * Finds the import util specifier related to Testing Library for a given name. - */ - const findImportedTestingLibraryUtilSpecifier: FindImportedTestingLibraryUtilSpecifierFn = - ( - specifierName - ): TSESTree.Identifier | TSESTree.ImportClause | undefined => { - const node = - getCustomModuleImportNode() ?? getTestingLibraryImportNode(); - - if (!node) { - return undefined; - } - - return findImportSpecifier(specifierName, node); - }; - - const findImportedUserEventSpecifier: () => TSESTree.Identifier | null = - () => { - if (!importedUserEventLibraryNode) { - return null; - } - - if (isImportDeclaration(importedUserEventLibraryNode)) { - const userEventIdentifier = - importedUserEventLibraryNode.specifiers.find((specifier) => - isImportDefaultSpecifier(specifier) - ); - - if (userEventIdentifier) { - return userEventIdentifier.local; - } - } else { - if ( - !ASTUtils.isVariableDeclarator(importedUserEventLibraryNode.parent) - ) { - return null; - } - - const requireNode = importedUserEventLibraryNode.parent; - if (!ASTUtils.isIdentifier(requireNode.id)) { - return null; - } - - return requireNode.id; - } - - return null; - }; - - const getTestingLibraryImportedUtilSpecifier = ( - node: TSESTree.Identifier | TSESTree.MemberExpression - ): TSESTree.Identifier | TSESTree.ImportClause | undefined => { - const identifierName: string | undefined = - getPropertyIdentifierNode(node)?.name; - - if (!identifierName) { - return undefined; - } - - return findImportedTestingLibraryUtilSpecifier(identifierName); - }; - - /** - * Determines if file inspected meets all conditions to be reported by rules or not. - */ - const canReportErrors: CanReportErrorsFn = () => { - return skipRuleReportingCheck || isTestingLibraryImported(); - }; - - /** - * Determines whether a node is imported from a valid Testing Library module - * - * This method will try to find any import matching the given node name, - * and also make sure the name is a valid match in case it's been renamed. - */ - const isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn = ( - node - ) => { - const importNode = getTestingLibraryImportedUtilSpecifier(node); - - if (!importNode) { - return false; - } - - const referenceNode = getReferenceNode(node); - const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode); - if (!referenceNodeIdentifier) { - return false; - } - - const importDeclaration = (() => { - if (isImportDeclaration(importNode.parent)) { - return importNode.parent; - } - - const variableDeclarator = - findClosestVariableDeclaratorNode(importNode); - - if (isCallExpression(variableDeclarator?.init)) { - return variableDeclarator?.init; - } - - return undefined; - })(); - - if (!importDeclaration) { - return false; - } - - const importDeclarationName = getImportModuleName(importDeclaration); - if (!importDeclarationName) { - return false; - } - - const identifierName: string | undefined = - getPropertyIdentifierNode(node)?.name; - - if (!identifierName) { - return false; - } - - const hasImportElementMatch = hasImportMatch(importNode, identifierName); - const hasImportModuleMatch = - /testing-library/g.test(importDeclarationName) || - (typeof customModuleSetting === 'string' && - importDeclarationName.endsWith(customModuleSetting)); - - return hasImportElementMatch && hasImportModuleMatch; - }; - - const helpers: DetectionHelpers = { - getTestingLibraryImportNode, - getCustomModuleImportNode, - getTestingLibraryImportName, - getCustomModuleImportName, - isTestingLibraryImported, - isTestingLibraryUtil, - isGetQueryVariant, - isQueryQueryVariant, - isFindQueryVariant, - isSyncQuery, - isAsyncQuery, - isQuery, - isCustomQuery, - isBuiltInQuery, - isAsyncUtil, - isFireEventUtil, - isUserEventUtil, - isFireEventMethod, - isUserEventMethod, - isRenderUtil, - isCreateEventUtil, - isRenderVariableDeclarator, - isDebugUtil, - isActUtil, - isPresenceAssert, - isAbsenceAssert, - canReportErrors, - findImportedTestingLibraryUtilSpecifier, - isNodeComingFromTestingLibrary, - }; - - // Instructions for Testing Library detection. - const detectionInstructions: TSESLint.RuleListener = { - /** - * This ImportDeclaration rule listener will check if Testing Library related - * modules are imported. Since imports happen first thing in a file, it's - * safe to use `isImportingTestingLibraryModule` and `isImportingCustomModule` - * since they will have corresponding value already updated when reporting other - * parts of the file. - */ - ImportDeclaration(node: TSESTree.ImportDeclaration) { - if (typeof node.source.value !== 'string') { - return; - } - // check only if testing library import not found yet so we avoid - // to override importedTestingLibraryNode after it's found - if ( - !importedTestingLibraryNode && - /testing-library/g.test(node.source.value) - ) { - importedTestingLibraryNode = node; - } - - // check only if custom module import not found yet so we avoid - // to override importedCustomModuleNode after it's found - const customModule = getCustomModule(); - if ( - customModule && - !importedCustomModuleNode && - node.source.value.endsWith(customModule) - ) { - importedCustomModuleNode = node; - } - - // check only if user-event import not found yet so we avoid - // to override importedUserEventLibraryNode after it's found - if ( - !importedUserEventLibraryNode && - node.source.value === USER_EVENT_PACKAGE - ) { - importedUserEventLibraryNode = node; - } - - // check only if react-dom/test-utils import not found yet so we avoid - // to override importedReactDomTestUtilsNode after it's found - if ( - !importedUserEventLibraryNode && - node.source.value === REACT_DOM_TEST_UTILS_PACKAGE - ) { - importedReactDomTestUtilsNode = node; - } - }, - - // Check if Testing Library related modules are loaded with required. - [`CallExpression > Identifier[name="require"]`]( - node: TSESTree.Identifier - ) { - const callExpression = node.parent as TSESTree.CallExpression; - const { arguments: args } = callExpression; - - if ( - !importedTestingLibraryNode && - args.some( - (arg) => - isLiteral(arg) && - typeof arg.value === 'string' && - /testing-library/g.test(arg.value) - ) - ) { - importedTestingLibraryNode = callExpression; - } - - const customModule = getCustomModule(); - if ( - !importedCustomModuleNode && - args.some( - (arg) => - customModule && - isLiteral(arg) && - typeof arg.value === 'string' && - arg.value.endsWith(customModule) - ) - ) { - importedCustomModuleNode = callExpression; - } - - if ( - !importedCustomModuleNode && - args.some( - (arg) => - isLiteral(arg) && - typeof arg.value === 'string' && - arg.value === USER_EVENT_PACKAGE - ) - ) { - importedUserEventLibraryNode = callExpression; - } - - if ( - !importedReactDomTestUtilsNode && - args.some( - (arg) => - isLiteral(arg) && - typeof arg.value === 'string' && - arg.value === REACT_DOM_TEST_UTILS_PACKAGE - ) - ) { - importedReactDomTestUtilsNode = callExpression; - } - }, - }; - - // update given rule to inject Testing Library detection - const ruleInstructions = ruleCreate(context, optionsWithDefault, helpers); - const enhancedRuleInstructions: TSESLint.RuleListener = {}; - - const allKeys = new Set( - Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)) - ); - - // Iterate over ALL instructions keys so we can override original rule instructions - // to prevent their execution if conditions to report errors are not met. - allKeys.forEach((instruction) => { - enhancedRuleInstructions[instruction] = (node) => { - if (instruction in detectionInstructions) { - detectionInstructions[instruction]?.(node); - } - - if (canReportErrors() && ruleInstructions[instruction]) { - return ruleInstructions[instruction]?.(node); - } - - return undefined; - }; - }); - - return enhancedRuleInstructions; - }; + return ( + context: TestingLibraryContext, + optionsWithDefault: Readonly + ): TSESLint.RuleListener => { + const importedTestingLibraryNodes: ImportModuleNode[] = []; + let importedCustomModuleNode: ImportModuleNode | null = null; + let importedUserEventLibraryNode: ImportModuleNode | null = null; + let importedReactDomTestUtilsNode: ImportModuleNode | null = null; + + // Init options based on shared ESLint settings + const customModuleSetting = + context.settings['testing-library/utils-module']; + const customRendersSetting = + context.settings['testing-library/custom-renders']; + const customQueriesSetting = + context.settings['testing-library/custom-queries']; + + /** + * Small method to extract common checks to determine whether a node is + * related to Testing Library or not. + * + * To determine whether a node is a valid Testing Library util, there are + * two conditions to match: + * - it's named in a particular way (decided by given callback) + * - it's imported from valid Testing Library module (depends on aggressive + * reporting) + */ + function isPotentialTestingLibraryFunction( + node: TSESTree.Identifier | null | undefined, + isPotentialFunctionCallback: ( + identifierNodeName: string, + originalNodeName?: string + ) => boolean + ): boolean { + if (!node) { + return false; + } + + const referenceNode = getReferenceNode(node); + const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode); + + if (!referenceNodeIdentifier) { + return false; + } + + const importedUtilSpecifier = getTestingLibraryImportedUtilSpecifier( + referenceNodeIdentifier + ); + + const originalNodeName = + isImportSpecifier(importedUtilSpecifier) && + ASTUtils.isIdentifier(importedUtilSpecifier.imported) && + importedUtilSpecifier.local.name !== importedUtilSpecifier.imported.name + ? importedUtilSpecifier.imported.name + : undefined; + + if (!isPotentialFunctionCallback(node.name, originalNodeName)) { + return false; + } + + if (isAggressiveModuleReportingEnabled()) { + return true; + } + + return isNodeComingFromTestingLibrary(referenceNodeIdentifier); + } + + /** + * Determines whether aggressive module reporting is enabled or not. + * + * This aggressive reporting mechanism is considered as enabled when custom + * module is not set, so we need to assume everything matching Testing + * Library utils is related to Testing Library no matter from where module + * they are coming from. Otherwise, this aggressive reporting mechanism is + * opted-out in favour to report only those utils coming from Testing + * Library package or custom module set up on settings. + */ + const isAggressiveModuleReportingEnabled = () => !customModuleSetting; + + /** + * Determines whether aggressive render reporting is enabled or not. + * + * This aggressive reporting mechanism is considered as enabled when custom + * renders are not set, so we need to assume every method containing + * "render" is a valid Testing Library `render`. Otherwise, this aggressive + * reporting mechanism is opted-out in favour to report only `render` or + * names set up on custom renders setting. + */ + const isAggressiveRenderReportingEnabled = (): boolean => { + const isSwitchedOff = customRendersSetting === SETTING_OPTION_OFF; + const hasCustomOptions = + Array.isArray(customRendersSetting) && customRendersSetting.length > 0; + + return !isSwitchedOff && !hasCustomOptions; + }; + + /** + * Determines whether Aggressive Reporting for queries is enabled or not. + * + * This Aggressive Reporting mechanism is considered as enabled when custom-queries setting is not set, + * so the plugin needs to report both built-in and custom queries. + * Otherwise, this Aggressive Reporting mechanism is opted-out in favour of reporting only built-in queries + those + * indicated in custom-queries setting. + */ + const isAggressiveQueryReportingEnabled = (): boolean => { + const isSwitchedOff = customQueriesSetting === SETTING_OPTION_OFF; + const hasCustomOptions = + Array.isArray(customQueriesSetting) && customQueriesSetting.length > 0; + + return !isSwitchedOff && !hasCustomOptions; + }; + + const getCustomModule = (): string | undefined => { + if ( + !isAggressiveModuleReportingEnabled() && + customModuleSetting !== SETTING_OPTION_OFF + ) { + return customModuleSetting; + } + return undefined; + }; + + const getCustomRenders = (): string[] => { + if ( + !isAggressiveRenderReportingEnabled() && + customRendersSetting !== SETTING_OPTION_OFF + ) { + return customRendersSetting as string[]; + } + + return []; + }; + + const getCustomQueries = (): string[] => { + if ( + !isAggressiveQueryReportingEnabled() && + customQueriesSetting !== SETTING_OPTION_OFF + ) { + return customQueriesSetting as string[]; + } + + return []; + }; + + // Helpers for Testing Library detection. + const getTestingLibraryImportNode: GetTestingLibraryImportNodeFn = () => { + return importedTestingLibraryNodes[0]; + }; + + const getAllTestingLibraryImportNodes: GetTestingLibraryImportNodesFn = + () => { + return importedTestingLibraryNodes; + }; + + const getCustomModuleImportNode: GetCustomModuleImportNodeFn = () => { + return importedCustomModuleNode; + }; + + const getTestingLibraryImportName: GetTestingLibraryImportNameFn = () => { + return getImportModuleName(importedTestingLibraryNodes[0]); + }; + + const getCustomModuleImportName: GetCustomModuleImportNameFn = () => { + return getImportModuleName(importedCustomModuleNode); + }; + + /** + * Determines whether Testing Library utils are imported or not for + * current file being analyzed. + * + * By default, it is ALWAYS considered as imported. This is what we call + * "aggressive reporting" so we don't miss TL utils reexported from + * custom modules. + * + * However, there is a setting to customize the module where TL utils can + * be imported from: "testing-library/utils-module". If this setting is enabled, + * then this method will return `true` ONLY IF a testing-library package + * or custom module are imported. + */ + const isTestingLibraryImported: IsTestingLibraryImportedFn = ( + isStrict = false + ) => { + const isSomeModuleImported = + importedTestingLibraryNodes.length !== 0 || !!importedCustomModuleNode; + + return ( + (!isStrict && isAggressiveModuleReportingEnabled()) || + isSomeModuleImported + ); + }; + + /** + * Determines whether a given node is a reportable query, + * either a built-in or a custom one. + * + * Depending on Aggressive Query Reporting setting, custom queries will be + * reportable or not. + */ + const isQuery: IsQueryFn = (node) => { + const hasQueryPattern = /^(get|query|find)(All)?By.+$/.test(node.name); + if (!hasQueryPattern) { + return false; + } + + if (isAggressiveQueryReportingEnabled()) { + return true; + } + + const customQueries = getCustomQueries(); + const isBuiltInQuery = ALL_QUERIES_COMBINATIONS.includes(node.name); + const isReportableCustomQuery = customQueries.some((pattern) => + new RegExp(pattern).test(node.name) + ); + return isBuiltInQuery || isReportableCustomQuery; + }; + + /** + * Determines whether a given node is `get*` query variant or not. + */ + const isGetQueryVariant: IsGetQueryVariantFn = (node) => { + return isQuery(node) && node.name.startsWith('get'); + }; + + /** + * Determines whether a given node is `query*` query variant or not. + */ + const isQueryQueryVariant: IsQueryQueryVariantFn = (node) => { + return isQuery(node) && node.name.startsWith('query'); + }; + + /** + * Determines whether a given node is `find*` query variant or not. + */ + const isFindQueryVariant: IsFindQueryVariantFn = (node) => { + return isQuery(node) && node.name.startsWith('find'); + }; + + /** + * Determines whether a given node is sync query or not. + */ + const isSyncQuery: IsSyncQueryFn = (node) => { + return isGetQueryVariant(node) || isQueryQueryVariant(node); + }; + + /** + * Determines whether a given node is async query or not. + */ + const isAsyncQuery: IsAsyncQueryFn = (node) => { + return isFindQueryVariant(node); + }; + + const isCustomQuery: IsCustomQueryFn = (node) => { + return isQuery(node) && !ALL_QUERIES_COMBINATIONS.includes(node.name); + }; + + const isBuiltInQuery = (node: TSESTree.Identifier): boolean => { + return isQuery(node) && ALL_QUERIES_COMBINATIONS.includes(node.name); + }; + + /** + * Determines whether a given node is a valid async util or not. + * + * A node will be interpreted as a valid async util based on two conditions: + * the name matches with some Testing Library async util, and the node is + * coming from Testing Library module. + * + * The latter depends on Aggressive module reporting: + * if enabled, then it doesn't matter from where the given node was imported + * from as it will be considered part of Testing Library. + * Otherwise, it means `custom-module` has been set up, so only those nodes + * coming from Testing Library will be considered as valid. + */ + const isAsyncUtil: IsAsyncUtilFn = (node, validNames = ASYNC_UTILS) => { + return isPotentialTestingLibraryFunction( + node, + (identifierNodeName, originalNodeName) => { + return ( + (validNames as string[]).includes(identifierNodeName) || + (!!originalNodeName && + (validNames as string[]).includes(originalNodeName)) + ); + } + ); + }; + + /** + * Determines whether a given node is fireEvent util itself or not. + * + * Not to be confused with {@link isFireEventMethod} + */ + const isFireEventUtil = (node: TSESTree.Identifier): boolean => { + return isPotentialTestingLibraryFunction( + node, + (identifierNodeName, originalNodeName) => { + return [identifierNodeName, originalNodeName].includes('fireEvent'); + } + ); + }; + + /** + * Determines whether a given node is userEvent util itself or not. + * + * Not to be confused with {@link isUserEventMethod} + */ + const isUserEventUtil = (node: TSESTree.Identifier): boolean => { + const userEvent = findImportedUserEventSpecifier(); + let userEventName: string | undefined; + + if (userEvent) { + userEventName = userEvent.name; + } else if (isAggressiveModuleReportingEnabled()) { + userEventName = USER_EVENT_NAME; + } + + if (!userEventName) { + return false; + } + + return node.name === userEventName; + }; + + /** + * Determines whether a given node is fireEvent method or not + */ + // eslint-disable-next-line complexity + const isFireEventMethod: IsFireEventMethodFn = (node) => { + const fireEventUtil = + findImportedTestingLibraryUtilSpecifier(FIRE_EVENT_NAME); + let fireEventUtilName: string | undefined; + + if (fireEventUtil) { + fireEventUtilName = ASTUtils.isIdentifier(fireEventUtil) + ? fireEventUtil.name + : fireEventUtil.local.name; + } else if (isAggressiveModuleReportingEnabled()) { + fireEventUtilName = FIRE_EVENT_NAME; + } + + if (!fireEventUtilName) { + return false; + } + + const parentMemberExpression: TSESTree.MemberExpression | undefined = + node.parent && isMemberExpression(node.parent) + ? node.parent + : undefined; + + const parentCallExpression: TSESTree.CallExpression | undefined = + node.parent && isCallExpression(node.parent) ? node.parent : undefined; + + if (!parentMemberExpression && !parentCallExpression) { + return false; + } + + // check fireEvent('method', node) usage + if (parentCallExpression) { + return [fireEventUtilName, FIRE_EVENT_NAME].includes(node.name); + } + + // we know it's defined at this point, but TS seems to think it is not + // so here I'm enforcing it once in order to avoid using "!" operator every time + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const definedParentMemberExpression = parentMemberExpression!; + + // check fireEvent.click() usage + const regularCall = + ASTUtils.isIdentifier(definedParentMemberExpression.object) && + isCallExpression(definedParentMemberExpression.parent) && + definedParentMemberExpression.object.name === fireEventUtilName && + node.name !== FIRE_EVENT_NAME && + node.name !== fireEventUtilName; + + // check testingLibraryUtils.fireEvent.click() usage + const wildcardCall = + isMemberExpression(definedParentMemberExpression.object) && + ASTUtils.isIdentifier(definedParentMemberExpression.object.object) && + definedParentMemberExpression.object.object.name === + fireEventUtilName && + ASTUtils.isIdentifier(definedParentMemberExpression.object.property) && + definedParentMemberExpression.object.property.name === + FIRE_EVENT_NAME && + node.name !== FIRE_EVENT_NAME && + node.name !== fireEventUtilName; + + // check testingLibraryUtils.fireEvent('click') + const wildcardCallWithCallExpression = + ASTUtils.isIdentifier(definedParentMemberExpression.object) && + definedParentMemberExpression.object.name === fireEventUtilName && + ASTUtils.isIdentifier(definedParentMemberExpression.property) && + definedParentMemberExpression.property.name === FIRE_EVENT_NAME && + !isMemberExpression(definedParentMemberExpression.parent) && + node.name === FIRE_EVENT_NAME && + node.name !== fireEventUtilName; + + return regularCall || wildcardCall || wildcardCallWithCallExpression; + }; + + const isUserEventMethod: IsUserEventMethodFn = (node) => { + const userEvent = findImportedUserEventSpecifier(); + let userEventName: string | undefined; + + if (userEvent) { + userEventName = userEvent.name; + } else if (isAggressiveModuleReportingEnabled()) { + userEventName = USER_EVENT_NAME; + } + + if (!userEventName) { + return false; + } + + const parentMemberExpression: TSESTree.MemberExpression | undefined = + node.parent && isMemberExpression(node.parent) + ? node.parent + : undefined; + + if (!parentMemberExpression) { + return false; + } + + // make sure that given node it's not userEvent object itself + if ( + [userEventName, USER_EVENT_NAME].includes(node.name) || + (ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === node.name) + ) { + return false; + } + + // check userEvent.click() usage + return ( + ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === userEventName + ); + }; + + /** + * Determines whether a given node is a valid render util or not. + * + * A node will be interpreted as a valid render based on two conditions: + * the name matches with a valid "render" option, and the node is coming + * from Testing Library module. This depends on: + * + * - Aggressive render reporting: if enabled, then every node name + * containing "render" will be assumed as Testing Library render util. + * Otherwise, it means `custom-modules` has been set up, so only those nodes + * named as "render" or some of the `custom-modules` options will be + * considered as Testing Library render util. + * - Aggressive module reporting: if enabled, then it doesn't matter from + * where the given node was imported from as it will be considered part of + * Testing Library. Otherwise, it means `custom-module` has been set up, so + * only those nodes coming from Testing Library will be considered as valid. + */ + const isRenderUtil: IsRenderUtilFn = (node) => + isPotentialTestingLibraryFunction( + node, + (identifierNodeName, originalNodeName) => { + if (isAggressiveRenderReportingEnabled()) { + return identifierNodeName.toLowerCase().includes(RENDER_NAME); + } + + return [RENDER_NAME, ...getCustomRenders()].some( + (validRenderName) => + validRenderName === identifierNodeName || + (Boolean(originalNodeName) && + validRenderName === originalNodeName) + ); + } + ); + + const isCreateEventUtil: IsCreateEventUtil = (node) => { + const isCreateEventCallback = ( + identifierNodeName: string, + originalNodeName?: string + ) => [identifierNodeName, originalNodeName].includes(CREATE_EVENT_NAME); + if ( + isCallExpression(node) && + isMemberExpression(node.callee) && + ASTUtils.isIdentifier(node.callee.object) + ) { + return isPotentialTestingLibraryFunction( + node.callee.object, + isCreateEventCallback + ); + } + + if ( + isCallExpression(node) && + isMemberExpression(node.callee) && + isMemberExpression(node.callee.object) && + ASTUtils.isIdentifier(node.callee.object.property) + ) { + return isPotentialTestingLibraryFunction( + node.callee.object.property, + isCreateEventCallback + ); + } + const identifier = getDeepestIdentifierNode(node); + return isPotentialTestingLibraryFunction( + identifier, + isCreateEventCallback + ); + }; + + const isRenderVariableDeclarator: IsRenderVariableDeclaratorFn = (node) => { + if (!node.init) { + return false; + } + const initIdentifierNode = getDeepestIdentifierNode(node.init); + + if (!initIdentifierNode) { + return false; + } + + return isRenderUtil(initIdentifierNode); + }; + + const isDebugUtil: IsDebugUtilFn = ( + identifierNode, + validNames = DEBUG_UTILS + ) => { + const isBuiltInConsole = + isMemberExpression(identifierNode.parent) && + ASTUtils.isIdentifier(identifierNode.parent.object) && + identifierNode.parent.object.name === 'console'; + + return ( + !isBuiltInConsole && + isPotentialTestingLibraryFunction( + identifierNode, + (identifierNodeName, originalNodeName) => { + return ( + (validNames as string[]).includes(identifierNodeName) || + (!!originalNodeName && + (validNames as string[]).includes(originalNodeName)) + ); + } + ) + ); + }; + + /** + * Determines whether a given node is some reportable `act` util. + * + * An `act` is reportable if some of these conditions is met: + * - it's related to Testing Library module (this depends on Aggressive Reporting) + * - it's related to React DOM Test Utils + */ + const isActUtil = (node: TSESTree.Identifier): boolean => { + const isTestingLibraryAct = isPotentialTestingLibraryFunction( + node, + (identifierNodeName, originalNodeName) => { + return [identifierNodeName, originalNodeName] + .filter(Boolean) + .includes('act'); + } + ); + + const isReactDomTestUtilsAct = (() => { + if (!importedReactDomTestUtilsNode) { + return false; + } + const referenceNode = getReferenceNode(node); + const referenceNodeIdentifier = + getPropertyIdentifierNode(referenceNode); + if (!referenceNodeIdentifier) { + return false; + } + + const importedUtilSpecifier = findImportSpecifier( + node.name, + importedReactDomTestUtilsNode + ); + if (!importedUtilSpecifier) { + return false; + } + + const importDeclaration = (() => { + if (isImportDeclaration(importedUtilSpecifier.parent)) { + return importedUtilSpecifier.parent; + } + + const variableDeclarator = findClosestVariableDeclaratorNode( + importedUtilSpecifier + ); + + if (isCallExpression(variableDeclarator?.init)) { + return variableDeclarator?.init; + } + + return undefined; + })(); + if (!importDeclaration) { + return false; + } + + const importDeclarationName = getImportModuleName(importDeclaration); + if (!importDeclarationName) { + return false; + } + + if (importDeclarationName !== REACT_DOM_TEST_UTILS_PACKAGE) { + return false; + } + + return hasImportMatch( + importedUtilSpecifier, + referenceNodeIdentifier.name + ); + })(); + + return isTestingLibraryAct || isReactDomTestUtilsAct; + }; + + const isTestingLibraryUtil = (node: TSESTree.Identifier): boolean => { + return ( + isAsyncUtil(node) || + isQuery(node) || + isRenderUtil(node) || + isFireEventMethod(node) || + isUserEventMethod(node) || + isActUtil(node) || + isCreateEventUtil(node) + ); + }; + + /** + * Determines whether a given MemberExpression node is a presence assert + * + * Presence asserts could have shape of: + * - expect(element).toBeInTheDocument() + * - expect(element).not.toBeNull() + */ + const isPresenceAssert: IsPresenceAssertFn = (node) => { + const { matcher, isNegated } = getAssertNodeInfo(node); + + if (!matcher) { + return false; + } + + return isNegated + ? ABSENCE_MATCHERS.some((absenceMather) => absenceMather === matcher) + : PRESENCE_MATCHERS.some( + (presenceMather) => presenceMather === matcher + ); + }; + + /** + * Determines whether a given MemberExpression node is an absence assert + * + * Absence asserts could have shape of: + * - expect(element).toBeNull() + * - expect(element).not.toBeInTheDocument() + */ + const isAbsenceAssert: IsAbsenceAssertFn = (node) => { + const { matcher, isNegated } = getAssertNodeInfo(node); + + if (!matcher) { + return false; + } + + return isNegated + ? PRESENCE_MATCHERS.some((presenceMather) => presenceMather === matcher) + : ABSENCE_MATCHERS.some((absenceMather) => absenceMather === matcher); + }; + + const isMatchingAssert: IsMatchingAssertFn = (node, matcherName) => { + const { matcher } = getAssertNodeInfo(node); + + if (!matcher) { + return false; + } + + return matcher === matcherName; + }; + + /** + * Finds the import util specifier related to Testing Library for a given name. + */ + const findImportedTestingLibraryUtilSpecifier: FindImportedTestingLibraryUtilSpecifierFn = + ( + specifierName + ): TSESTree.Identifier | TSESTree.ImportClause | undefined => { + const node = + getCustomModuleImportNode() ?? getTestingLibraryImportNode(); + + if (!node) { + return undefined; + } + + return findImportSpecifier(specifierName, node); + }; + + const findImportedUserEventSpecifier: () => TSESTree.Identifier | null = + () => { + if (!importedUserEventLibraryNode) { + return null; + } + + if (isImportDeclaration(importedUserEventLibraryNode)) { + const userEventIdentifier = + importedUserEventLibraryNode.specifiers.find((specifier) => + isImportDefaultSpecifier(specifier) + ); + + if (userEventIdentifier) { + return userEventIdentifier.local; + } + } else { + if ( + !ASTUtils.isVariableDeclarator(importedUserEventLibraryNode.parent) + ) { + return null; + } + + const requireNode = importedUserEventLibraryNode.parent; + if (!ASTUtils.isIdentifier(requireNode.id)) { + return null; + } + + return requireNode.id; + } + + return null; + }; + + const getTestingLibraryImportedUtilSpecifier = ( + node: TSESTree.Identifier | TSESTree.MemberExpression + ): TSESTree.Identifier | TSESTree.ImportClause | undefined => { + const identifierName: string | undefined = + getPropertyIdentifierNode(node)?.name; + + if (!identifierName) { + return undefined; + } + + return findImportedTestingLibraryUtilSpecifier(identifierName); + }; + + /** + * Determines if file inspected meets all conditions to be reported by rules or not. + */ + const canReportErrors: CanReportErrorsFn = () => { + return skipRuleReportingCheck || isTestingLibraryImported(); + }; + + /** + * Determines whether a node is imported from a valid Testing Library module + * + * This method will try to find any import matching the given node name, + * and also make sure the name is a valid match in case it's been renamed. + */ + const isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn = ( + node + ) => { + const importNode = getTestingLibraryImportedUtilSpecifier(node); + + if (!importNode) { + return false; + } + + const referenceNode = getReferenceNode(node); + const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode); + if (!referenceNodeIdentifier) { + return false; + } + + const importDeclaration = (() => { + if (isImportDeclaration(importNode.parent)) { + return importNode.parent; + } + + const variableDeclarator = + findClosestVariableDeclaratorNode(importNode); + + if (isCallExpression(variableDeclarator?.init)) { + return variableDeclarator?.init; + } + + return undefined; + })(); + + if (!importDeclaration) { + return false; + } + + const importDeclarationName = getImportModuleName(importDeclaration); + if (!importDeclarationName) { + return false; + } + + const identifierName: string | undefined = + getPropertyIdentifierNode(node)?.name; + + if (!identifierName) { + return false; + } + + const hasImportElementMatch = hasImportMatch(importNode, identifierName); + + return ( + hasImportElementMatch && + isTestingLibraryModule(importDeclarationName, customModuleSetting) + ); + }; + + const helpers: DetectionHelpers = { + getTestingLibraryImportNode, + getAllTestingLibraryImportNodes, + getCustomModuleImportNode, + getTestingLibraryImportName, + getCustomModuleImportName, + isTestingLibraryImported, + isTestingLibraryUtil, + isGetQueryVariant, + isQueryQueryVariant, + isFindQueryVariant, + isSyncQuery, + isAsyncQuery, + isQuery, + isCustomQuery, + isBuiltInQuery, + isAsyncUtil, + isFireEventUtil, + isUserEventUtil, + isFireEventMethod, + isUserEventMethod, + isRenderUtil, + isCreateEventUtil, + isRenderVariableDeclarator, + isDebugUtil, + isActUtil, + isPresenceAssert, + isMatchingAssert, + isAbsenceAssert, + canReportErrors, + findImportedTestingLibraryUtilSpecifier, + isNodeComingFromTestingLibrary, + }; + + // Instructions for Testing Library detection. + const detectionInstructions: TSESLint.RuleListener = { + /** + * This ImportDeclaration rule listener will check if Testing Library related + * modules are imported. Since imports happen first thing in a file, it's + * safe to use `isImportingTestingLibraryModule` and `isImportingCustomModule` + * since they will have corresponding value already updated when reporting other + * parts of the file. + */ + ImportDeclaration(node: TSESTree.ImportDeclaration) { + if (typeof node.source.value !== 'string') { + return; + } + // check only if testing library import not found yet so we avoid + // to override importedTestingLibraryNodes after it's found + if (isOfficialTestingLibraryModule(node.source.value)) { + importedTestingLibraryNodes.push(node); + } + + // check only if custom module import not found yet so we avoid + // to override importedCustomModuleNode after it's found + const customModule = getCustomModule(); + if ( + !importedCustomModuleNode && + isCustomTestingLibraryModule(node.source.value, customModule) + ) { + importedCustomModuleNode = node; + } + + // check only if user-event import not found yet so we avoid + // to override importedUserEventLibraryNode after it's found + if ( + !importedUserEventLibraryNode && + node.source.value === USER_EVENT_MODULE + ) { + importedUserEventLibraryNode = node; + } + + // check only if react-dom/test-utils import not found yet so we avoid + // to override importedReactDomTestUtilsNode after it's found + if ( + !importedUserEventLibraryNode && + node.source.value === REACT_DOM_TEST_UTILS_PACKAGE + ) { + importedReactDomTestUtilsNode = node; + } + }, + + // Check if Testing Library related modules are loaded with required. + [`CallExpression > Identifier[name="require"]`]( + node: TSESTree.Identifier + ) { + const callExpression = node.parent as TSESTree.CallExpression; + const { arguments: args } = callExpression; + + if ( + args.some( + (arg) => + isLiteral(arg) && + typeof arg.value === 'string' && + isOfficialTestingLibraryModule(arg.value) + ) + ) { + importedTestingLibraryNodes.push(callExpression); + } + + const customModule = getCustomModule(); + if ( + !importedCustomModuleNode && + args.some( + (arg) => + isLiteral(arg) && + typeof arg.value === 'string' && + isCustomTestingLibraryModule(arg.value, customModule) + ) + ) { + importedCustomModuleNode = callExpression; + } + + if ( + !importedCustomModuleNode && + args.some( + (arg) => + isLiteral(arg) && + typeof arg.value === 'string' && + arg.value === USER_EVENT_MODULE + ) + ) { + importedUserEventLibraryNode = callExpression; + } + + if ( + !importedReactDomTestUtilsNode && + args.some( + (arg) => + isLiteral(arg) && + typeof arg.value === 'string' && + arg.value === REACT_DOM_TEST_UTILS_PACKAGE + ) + ) { + importedReactDomTestUtilsNode = callExpression; + } + }, + }; + + // update given rule to inject Testing Library detection + const ruleInstructions = ruleCreate(context, optionsWithDefault, helpers); + const enhancedRuleInstructions: TSESLint.RuleListener = {}; + + const allKeys = new Set( + Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)) + ); + + // Iterate over ALL instructions keys so we can override original rule instructions + // to prevent their execution if conditions to report errors are not met. + allKeys.forEach((instruction) => { + enhancedRuleInstructions[instruction] = (node) => { + if (instruction in detectionInstructions) { + detectionInstructions[instruction]?.(node); + } + + if (canReportErrors() && ruleInstructions[instruction]) { + return ruleInstructions[instruction]?.(node); + } + + return undefined; + }; + }); + + return enhancedRuleInstructions; + }; } diff --git a/lib/create-testing-library-rule/index.ts b/lib/create-testing-library-rule/index.ts index df46b06d..8ce3b334 100644 --- a/lib/create-testing-library-rule/index.ts +++ b/lib/create-testing-library-rule/index.ts @@ -1,44 +1,50 @@ -import { ESLintUtils, TSESLint } from '@typescript-eslint/experimental-utils'; +import { ESLintUtils } from '@typescript-eslint/utils'; -import { getDocsUrl, TestingLibraryRuleMeta } from '../utils'; +import { + getDocsUrl, + TestingLibraryPluginDocs, + TestingLibraryPluginRuleModule, +} from '../utils'; import { - DetectionOptions, - detectTestingLibraryUtils, - EnhancedRuleCreate, + DetectionOptions, + detectTestingLibraryUtils, + EnhancedRuleCreate, } from './detect-testing-library-utils'; -export function createTestingLibraryRule< - TOptions extends readonly unknown[], - TMessageIds extends string, - TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener +export const createTestingLibraryRule = < + TOptions extends readonly unknown[], + TMessageIds extends string, >({ - create, - detectionOptions = {}, - meta, - ...remainingConfig -}: Readonly<{ - name: string; - meta: TestingLibraryRuleMeta; - defaultOptions: Readonly; - detectionOptions?: Partial; - create: EnhancedRuleCreate; -}>): TSESLint.RuleModule { - // eslint-disable-next-line @babel/new-cap - return ESLintUtils.RuleCreator(getDocsUrl)({ - ...remainingConfig, - create: detectTestingLibraryUtils( - 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, - }, - }, - }); -} + create, + detectionOptions = {}, + ...remainingConfig +}: Readonly< + Omit< + ESLintUtils.RuleWithMetaAndName< + TOptions, + TMessageIds, + TestingLibraryPluginDocs + >, + 'create' + > & { + create: EnhancedRuleCreate; + detectionOptions?: Partial; + } +>): TestingLibraryPluginRuleModule => { + const rule = ESLintUtils.RuleCreator>( + getDocsUrl + )({ + ...remainingConfig, + create: detectTestingLibraryUtils( + create, + detectionOptions + ), + }); + const { docs } = rule.meta; + if (docs === undefined) { + throw new Error('Rule metadata must contain `docs` property'); + } + + return { ...rule, meta: { ...rule.meta, docs } }; +}; diff --git a/lib/index.ts b/lib/index.ts index e288411a..bb6f4db6 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,7 +1,44 @@ +import type { TSESLint } from '@typescript-eslint/utils'; + import configs from './configs'; import rules from './rules'; +import { SupportedTestingFramework } from './utils'; + +// we can't natively import package.json as tsc will copy it into dist/ +const { + name: packageName, + version: packageVersion, + // eslint-disable-next-line @typescript-eslint/no-require-imports +} = require('../package.json') as { name: string; version: string }; -export = { - configs, - rules, +const plugin = { + meta: { + name: packageName, + version: packageVersion, + }, + // ugly cast for now to keep TypeScript happy since + // we don't have types for flat config yet + configs: {} as Record< + SupportedTestingFramework | `flat/${SupportedTestingFramework}`, + TSESLint.SharedConfig.RulesRecord + >, + rules, }; + +plugin.configs = { + ...configs, + ...(Object.fromEntries( + Object.entries(configs).map(([framework, config]) => [ + `flat/${framework}`, + { + plugins: { 'testing-library': plugin }, + rules: config.rules, + }, + ]) + ) as unknown as Record< + `flat/${SupportedTestingFramework}`, + TSESLint.SharedConfig.RulesRecord & { plugins: unknown } + >), +}; + +export = plugin; diff --git a/lib/node-utils/accessors.ts b/lib/node-utils/accessors.ts new file mode 100644 index 00000000..e9ba2ee5 --- /dev/null +++ b/lib/node-utils/accessors.ts @@ -0,0 +1,127 @@ +import { + AST_NODE_TYPES, + ASTUtils, + type TSESTree, +} from '@typescript-eslint/utils'; + +import { isLiteral, isTemplateLiteral } from './is-node-of-type'; + +/** + * A `Literal` with a `value` of type `string`. + */ +interface StringLiteral + extends TSESTree.StringLiteral { + value: Value; +} + +/** + * Checks if the given `node` is a `StringLiteral`. + * + * If a `value` is provided & the `node` is a `StringLiteral`, + * the `value` will be compared to that of the `StringLiteral`. + */ +const isStringLiteral = ( + node: TSESTree.Node, + value?: V +): node is StringLiteral => + isLiteral(node) && + typeof node.value === 'string' && + (value === undefined || node.value === value); + +interface TemplateLiteral + extends TSESTree.TemplateLiteral { + quasis: [TSESTree.TemplateElement & { value: { raw: Value; cooked: Value } }]; +} + +/** + * Checks if the given `node` is a `TemplateLiteral`. + * + * Complex `TemplateLiteral`s are not considered specific, and so will return `false`. + * + * If a `value` is provided & the `node` is a `TemplateLiteral`, + * the `value` will be compared to that of the `TemplateLiteral`. + */ +const isSimpleTemplateLiteral = ( + node: TSESTree.Node, + value?: V +): node is TemplateLiteral => + isTemplateLiteral(node) && + node.quasis.length === 1 && // bail out if not simple + (value === undefined || node.quasis[0].value.raw === value); + +export type StringNode = + | StringLiteral + | TemplateLiteral; + +/** + * Checks if the given `node` is a {@link StringNode}. + */ +export const isStringNode = ( + node: TSESTree.Node, + specifics?: V +): node is StringNode => + isStringLiteral(node, specifics) || isSimpleTemplateLiteral(node, specifics); + +/** + * Gets the value of the given `StringNode`. + * + * If the `node` is a `TemplateLiteral`, the `raw` value is used; + * otherwise, `value` is returned instead. + */ +export const getStringValue = (node: StringNode): S => + isSimpleTemplateLiteral(node) ? node.quasis[0].value.raw : node.value; + +/** + * An `Identifier` with a known `name` value + */ +interface KnownIdentifier extends TSESTree.Identifier { + name: Name; +} + +/** + * Checks if the given `node` is an `Identifier`. + * + * If a `name` is provided, & the `node` is an `Identifier`, + * the `name` will be compared to that of the `identifier`. + */ +export const isIdentifier = ( + node: TSESTree.Node, + name?: V +): node is KnownIdentifier => + ASTUtils.isIdentifier(node) && (name === undefined || node.name === name); + +/** + * Checks if the given `node` is a "supported accessor". + * + * This means that it's a node can be used to access properties, + * and who's "value" can be statically determined. + * + * `MemberExpression` nodes most commonly contain accessors, + * but it's possible for other nodes to contain them. + * + * If a `value` is provided & the `node` is an `AccessorNode`, + * the `value` will be compared to that of the `AccessorNode`. + * + * Note that `value` here refers to the normalised value. + * The property that holds the value is not always called `name`. + */ +export const isSupportedAccessor = ( + node: TSESTree.Node, + value?: V +): node is AccessorNode => + isIdentifier(node, value) || isStringNode(node, value); + +/** + * Gets the value of the given `AccessorNode`, + * account for the different node types. + */ +export const getAccessorValue = ( + accessor: AccessorNode +): S => + accessor.type === AST_NODE_TYPES.Identifier + ? accessor.name + : getStringValue(accessor); + +export type AccessorNode = + | StringNode + | KnownIdentifier; diff --git a/lib/node-utils/index.ts b/lib/node-utils/index.ts index d1718f01..86dba929 100644 --- a/lib/node-utils/index.ts +++ b/lib/node-utils/index.ts @@ -1,57 +1,61 @@ +import { FunctionScope, ScopeType } from '@typescript-eslint/scope-manager'; import { - AST_NODE_TYPES, - ASTUtils, - TSESLint, - TSESLintScope, - TSESTree, -} from '@typescript-eslint/experimental-utils'; + AST_NODE_TYPES, + ASTUtils, + TSESLint, + TSESTree, +} from '@typescript-eslint/utils'; + +import { getDeclaredVariables, getScope } from '../utils'; import { - isArrayExpression, - isArrowFunctionExpression, - isAssignmentExpression, - isBlockStatement, - isCallExpression, - isExpressionStatement, - isImportDeclaration, - isImportNamespaceSpecifier, - isImportSpecifier, - isLiteral, - isMemberExpression, - isObjectPattern, - isProperty, - isReturnStatement, - isVariableDeclaration, + isArrayExpression, + isArrowFunctionExpression, + isAssignmentExpression, + isBlockStatement, + isCallExpression, + isExpressionStatement, + isFunctionExpression, + isFunctionDeclaration, + isImportDeclaration, + isImportNamespaceSpecifier, + isImportSpecifier, + isLiteral, + isMemberExpression, + isObjectPattern, + isProperty, + isReturnStatement, + isVariableDeclaration, } from './is-node-of-type'; export * from './is-node-of-type'; const ValidLeftHandSideExpressions = [ - AST_NODE_TYPES.CallExpression, - AST_NODE_TYPES.ClassExpression, - AST_NODE_TYPES.ClassDeclaration, - AST_NODE_TYPES.FunctionExpression, - AST_NODE_TYPES.Literal, - AST_NODE_TYPES.TemplateLiteral, - AST_NODE_TYPES.MemberExpression, - AST_NODE_TYPES.ArrayExpression, - AST_NODE_TYPES.ArrayPattern, - AST_NODE_TYPES.ClassExpression, - AST_NODE_TYPES.FunctionExpression, - AST_NODE_TYPES.Identifier, - AST_NODE_TYPES.JSXElement, - AST_NODE_TYPES.JSXFragment, - AST_NODE_TYPES.JSXOpeningElement, - AST_NODE_TYPES.MetaProperty, - AST_NODE_TYPES.ObjectExpression, - AST_NODE_TYPES.ObjectPattern, - AST_NODE_TYPES.Super, - AST_NODE_TYPES.ThisExpression, - AST_NODE_TYPES.TSNullKeyword, - AST_NODE_TYPES.TaggedTemplateExpression, - AST_NODE_TYPES.TSNonNullExpression, - AST_NODE_TYPES.TSAsExpression, - AST_NODE_TYPES.ArrowFunctionExpression, + AST_NODE_TYPES.CallExpression, + AST_NODE_TYPES.ClassExpression, + AST_NODE_TYPES.ClassDeclaration, + AST_NODE_TYPES.FunctionExpression, + AST_NODE_TYPES.Literal, + AST_NODE_TYPES.TemplateLiteral, + AST_NODE_TYPES.MemberExpression, + AST_NODE_TYPES.ArrayExpression, + AST_NODE_TYPES.ArrayPattern, + AST_NODE_TYPES.ClassExpression, + AST_NODE_TYPES.FunctionExpression, + AST_NODE_TYPES.Identifier, + AST_NODE_TYPES.JSXElement, + AST_NODE_TYPES.JSXFragment, + AST_NODE_TYPES.JSXOpeningElement, + AST_NODE_TYPES.MetaProperty, + AST_NODE_TYPES.ObjectExpression, + AST_NODE_TYPES.ObjectPattern, + AST_NODE_TYPES.Super, + AST_NODE_TYPES.ThisExpression, + AST_NODE_TYPES.TSNullKeyword, + AST_NODE_TYPES.TaggedTemplateExpression, + AST_NODE_TYPES.TSNonNullExpression, + AST_NODE_TYPES.TSAsExpression, + AST_NODE_TYPES.ArrowFunctionExpression, ]; /** @@ -60,125 +64,147 @@ const ValidLeftHandSideExpressions = [ * @param shouldRestrictInnerScope - If true, CallExpression must belong to innermost scope of given node */ export function findClosestCallExpressionNode( - node: TSESTree.Node | null | undefined, - shouldRestrictInnerScope = false + node: TSESTree.Node | null | undefined, + shouldRestrictInnerScope = false ): TSESTree.CallExpression | null { - if (isCallExpression(node)) { - return node; - } - - if (!node || !node.parent) { - return null; - } - - if ( - shouldRestrictInnerScope && - !ValidLeftHandSideExpressions.includes(node.parent.type) - ) { - return null; - } - - return findClosestCallExpressionNode(node.parent, shouldRestrictInnerScope); + if (isCallExpression(node)) { + return node; + } + + if (!node?.parent) { + return null; + } + + if ( + shouldRestrictInnerScope && + !ValidLeftHandSideExpressions.includes(node.parent.type) + ) { + return null; + } + + return findClosestCallExpressionNode(node.parent, shouldRestrictInnerScope); } export function findClosestVariableDeclaratorNode( - node: TSESTree.Node | undefined + node: TSESTree.Node | undefined ): TSESTree.VariableDeclarator | null { - if (!node) { - return null; - } + if (!node) { + return null; + } + + if (ASTUtils.isVariableDeclarator(node)) { + return node; + } - if (ASTUtils.isVariableDeclarator(node)) { - return node; - } + return findClosestVariableDeclaratorNode(node.parent); +} - return findClosestVariableDeclaratorNode(node.parent); +export function findClosestFunctionExpressionNode( + node: TSESTree.Node | undefined +): + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression + | TSESTree.FunctionDeclaration + | null { + if (!node) { + return null; + } + + if ( + isArrowFunctionExpression(node) || + isFunctionExpression(node) || + isFunctionDeclaration(node) + ) { + return node; + } + + return findClosestFunctionExpressionNode(node.parent); } /** * TODO: remove this one in favor of {@link findClosestCallExpressionNode} */ export function findClosestCallNode( - node: TSESTree.Node, - name: string + node: TSESTree.Node, + name: string ): TSESTree.CallExpression | null { - if (!node.parent) { - return null; - } - - if ( - isCallExpression(node) && - ASTUtils.isIdentifier(node.callee) && - node.callee.name === name - ) { - return node; - } else { - return findClosestCallNode(node.parent, name); - } + if (!node.parent) { + return null; + } + + if ( + isCallExpression(node) && + ASTUtils.isIdentifier(node.callee) && + node.callee.name === name + ) { + return node; + } else { + return findClosestCallNode(node.parent, name); + } } export function hasThenProperty(node: TSESTree.Node): boolean { - return ( - isMemberExpression(node) && - ASTUtils.isIdentifier(node.property) && - node.property.name === 'then' - ); + return ( + isMemberExpression(node) && + ASTUtils.isIdentifier(node.property) && + node.property.name === 'then' + ); } export function hasChainedThen(node: TSESTree.Node): boolean { - const parent = node.parent; + const parent = node.parent; - // wait(...).then(...) - if (isCallExpression(parent) && parent.parent) { - return hasThenProperty(parent.parent); - } + // wait(...).then(...) + if (isCallExpression(parent) && parent.parent) { + return hasThenProperty(parent.parent); + } - // promise.then(...) - return !!parent && hasThenProperty(parent); + // promise.then(...) + return !!parent && hasThenProperty(parent); } export function isPromiseIdentifier( - node: TSESTree.Node + node: TSESTree.Node ): node is TSESTree.Identifier & { name: 'Promise' } { - return ASTUtils.isIdentifier(node) && node.name === 'Promise'; + return ASTUtils.isIdentifier(node) && node.name === 'Promise'; } export function isPromiseAll(node: TSESTree.CallExpression): boolean { - return ( - isMemberExpression(node.callee) && - isPromiseIdentifier(node.callee.object) && - ASTUtils.isIdentifier(node.callee.property) && - node.callee.property.name === 'all' - ); + return ( + isMemberExpression(node.callee) && + isPromiseIdentifier(node.callee.object) && + ASTUtils.isIdentifier(node.callee.property) && + node.callee.property.name === 'all' + ); } export function isPromiseAllSettled(node: TSESTree.CallExpression): boolean { - return ( - isMemberExpression(node.callee) && - isPromiseIdentifier(node.callee.object) && - ASTUtils.isIdentifier(node.callee.property) && - node.callee.property.name === 'allSettled' - ); + return ( + isMemberExpression(node.callee) && + isPromiseIdentifier(node.callee.object) && + ASTUtils.isIdentifier(node.callee.property) && + node.callee.property.name === 'allSettled' + ); } /** - * Determines whether a given node belongs to handled Promise.all or Promise.allSettled + * Determines whether a given node belongs to handled `Promise.all` or `Promise.allSettled` * array expression. */ export function isPromisesArrayResolved(node: TSESTree.Node): boolean { - const closestCallExpression = findClosestCallExpressionNode(node, true); - - if (!closestCallExpression) { - return false; - } - - return ( - !!closestCallExpression.parent && - isArrayExpression(closestCallExpression.parent) && - isCallExpression(closestCallExpression.parent.parent) && - (isPromiseAll(closestCallExpression.parent.parent) || - isPromiseAllSettled(closestCallExpression.parent.parent)) - ); + const closestCallExpression = findClosestCallExpressionNode(node, true); + + if (!closestCallExpression) { + return false; + } + + return ( + !!closestCallExpression.parent && + isArrayExpression(closestCallExpression.parent) && + isCallExpression(closestCallExpression.parent.parent) && + (isPromiseAll(closestCallExpression.parent.parent) || + isPromiseAllSettled(closestCallExpression.parent.parent)) + ); } /** @@ -191,108 +217,137 @@ export function isPromisesArrayResolved(node: TSESTree.Node): boolean { * - it's chained with the `then` method * - it's returned from a function * - has `resolves` or `rejects` jest methods + * - has `toResolve` or `toReject` jest-extended matchers + * - has a jasmine async matcher */ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean { - const closestCallExpressionNode = findClosestCallExpressionNode( - nodeIdentifier, - true - ); - - const suspiciousNodes = [nodeIdentifier, closestCallExpressionNode].filter( - Boolean - ); - - for (const node of suspiciousNodes) { - if (!node || !node.parent) { - continue; - } - if (ASTUtils.isAwaitExpression(node.parent)) { - return true; - } - - if ( - isArrowFunctionExpression(node.parent) || - isReturnStatement(node.parent) - ) { - return true; - } - - if (hasClosestExpectResolvesRejects(node.parent)) { - return true; - } - - if (hasChainedThen(node)) { - return true; - } - - if (isPromisesArrayResolved(node)) { - return true; - } - } - - return false; + const closestCallExpressionNode = findClosestCallExpressionNode( + nodeIdentifier, + true + ); + const callRootExpression = + closestCallExpressionNode == null + ? null + : getRootExpression(closestCallExpressionNode); + + const suspiciousNodes = [nodeIdentifier, callRootExpression].filter( + (node): node is NonNullable => node != null + ); + + return suspiciousNodes.some((node) => { + if (!node.parent) return false; + if (ASTUtils.isAwaitExpression(node.parent)) return true; + if ( + isArrowFunctionExpression(node.parent) || + isReturnStatement(node.parent) + ) + return true; + if (hasClosestExpectHandlesPromise(node.parent)) return true; + if (hasChainedThen(node)) return true; + if (isPromisesArrayResolved(node)) return true; + }); +} + +/** + * For an expression in a parent that evaluates to the expression or another child returns the parent node recursively. + */ +function getRootExpression( + expression: TSESTree.Expression +): TSESTree.Expression { + const { parent } = expression; + if (parent == null) return expression; + switch (parent.type) { + case AST_NODE_TYPES.ConditionalExpression: + return getRootExpression(parent); + case AST_NODE_TYPES.LogicalExpression: { + let rootExpression; + switch (parent.operator) { + case '??': + case '||': + rootExpression = getRootExpression(parent); + break; + case '&&': + rootExpression = + parent.right === expression + ? getRootExpression(parent) + : expression; + break; + } + return rootExpression ?? expression; + } + case AST_NODE_TYPES.SequenceExpression: + return parent.expressions[parent.expressions.length - 1] === expression + ? getRootExpression(parent) + : expression; + + case AST_NODE_TYPES.ChainExpression: + return getRootExpression(parent); + + default: + return expression; + } } export function getVariableReferences( - context: TSESLint.RuleContext, - node: TSESTree.Node + context: TSESLint.RuleContext, + node: TSESTree.Node ): TSESLint.Scope.Reference[] { - if (ASTUtils.isVariableDeclarator(node)) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - return context.getDeclaredVariables(node)[0]?.references?.slice(1) ?? []; - } + if (ASTUtils.isVariableDeclarator(node)) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return getDeclaredVariables(context, node)[0]?.references?.slice(1) ?? []; + } - return []; + return []; } -interface InnermostFunctionScope extends TSESLintScope.FunctionScope { - block: - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression; +interface InnermostFunctionScope extends FunctionScope { + block: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression; } export function getInnermostFunctionScope( - context: TSESLint.RuleContext, - asyncQueryNode: TSESTree.Identifier + context: TSESLint.RuleContext, + asyncQueryNode: TSESTree.Identifier ): InnermostFunctionScope | null { - const innermostScope = ASTUtils.getInnermostScope( - context.getScope(), - asyncQueryNode - ); - - if ( - innermostScope.type === 'function' && - ASTUtils.isFunction(innermostScope.block) - ) { - return innermostScope as unknown as InnermostFunctionScope; - } - - return null; + const innermostScope = ASTUtils.getInnermostScope( + getScope(context, asyncQueryNode), + asyncQueryNode + ); + + if ( + innermostScope.type === ScopeType.function && + ASTUtils.isFunction(innermostScope.block) + ) { + return innermostScope as unknown as InnermostFunctionScope; + } + + return null; } export function getFunctionReturnStatementNode( - functionNode: - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression + functionNode: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression ): TSESTree.Node | null { - if (isBlockStatement(functionNode.body)) { - // regular function or arrow function with block - const returnStatementNode = functionNode.body.body.find((statement) => - isReturnStatement(statement) - ) as TSESTree.ReturnStatement | undefined; - - if (!returnStatementNode) { - return null; - } - return returnStatementNode.argument; - } else if (functionNode.expression) { - // arrow function with implicit return - return functionNode.body; - } - - return null; + if (isBlockStatement(functionNode.body)) { + // regular function or arrow function with block + const returnStatementNode = functionNode.body.body.find((statement) => + isReturnStatement(statement) + ); + + if (!returnStatementNode) { + return null; + } + return returnStatementNode.argument; + } else if (functionNode.expression) { + // arrow function with implicit return + return functionNode.body; + } + + return null; } /** @@ -306,25 +361,29 @@ export function getFunctionReturnStatementNode( * it will return `rtl` identifier node */ export function getPropertyIdentifierNode( - node: TSESTree.Node + node: TSESTree.Node ): TSESTree.Identifier | null { - if (ASTUtils.isIdentifier(node)) { - return node; - } + if (ASTUtils.isIdentifier(node)) { + return node; + } + + if (isMemberExpression(node)) { + return getPropertyIdentifierNode(node.object); + } - if (isMemberExpression(node)) { - return getPropertyIdentifierNode(node.object); - } + if (isCallExpression(node)) { + return getPropertyIdentifierNode(node.callee); + } - if (isCallExpression(node)) { - return getPropertyIdentifierNode(node.callee); - } + if (isExpressionStatement(node)) { + return getPropertyIdentifierNode(node.expression); + } - if (isExpressionStatement(node)) { - return getPropertyIdentifierNode(node.expression); - } + if (ASTUtils.isAwaitExpression(node)) { + return getPropertyIdentifierNode(node.argument); + } - return null; + return null; } /** @@ -338,25 +397,25 @@ export function getPropertyIdentifierNode( * it will return `getByRole` identifier */ export function getDeepestIdentifierNode( - node: TSESTree.Node + node: TSESTree.Node ): TSESTree.Identifier | null { - if (ASTUtils.isIdentifier(node)) { - return node; - } + if (ASTUtils.isIdentifier(node)) { + return node; + } - if (isMemberExpression(node) && ASTUtils.isIdentifier(node.property)) { - return node.property; - } + if (isMemberExpression(node) && ASTUtils.isIdentifier(node.property)) { + return node.property; + } - if (isCallExpression(node)) { - return getDeepestIdentifierNode(node.callee); - } + if (isCallExpression(node)) { + return getDeepestIdentifierNode(node.callee); + } - if (ASTUtils.isAwaitExpression(node)) { - return getDeepestIdentifierNode(node.argument); - } + if (ASTUtils.isAwaitExpression(node)) { + return getDeepestIdentifierNode(node.argument); + } - return null; + return null; } /** @@ -370,213 +429,230 @@ export function getDeepestIdentifierNode( * it will return `rtl` node */ export function getReferenceNode( - node: - | TSESTree.CallExpression - | TSESTree.Identifier - | TSESTree.MemberExpression + node: + | TSESTree.CallExpression + | TSESTree.Identifier + | TSESTree.MemberExpression ): TSESTree.CallExpression | TSESTree.Identifier | TSESTree.MemberExpression { - if ( - node.parent && - (isMemberExpression(node.parent) || isCallExpression(node.parent)) - ) { - return getReferenceNode(node.parent); - } - - return node; + if ( + node.parent && + (isMemberExpression(node.parent) || isCallExpression(node.parent)) + ) { + return getReferenceNode(node.parent); + } + + return node; } export function getFunctionName( - node: - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression ): string { - return ( - ASTUtils.getFunctionNameWithKind(node) - .match(/('\w+')/g)?.[0] - .replace(/'/g, '') ?? '' - ); + return ( + ASTUtils.getFunctionNameWithKind(node) + .match(/('\w+')/g)?.[0] + .replace(/'/g, '') ?? '' + ); } // TODO: extract into types file? export type ImportModuleNode = - | TSESTree.CallExpression - | TSESTree.ImportDeclaration; + | TSESTree.CallExpression + | TSESTree.ImportDeclaration; export function getImportModuleName( - node: ImportModuleNode | null | undefined + node: ImportModuleNode | null | undefined ): string | undefined { - // import node of shape: import { foo } from 'bar' - if (isImportDeclaration(node) && typeof node.source.value === 'string') { - return node.source.value; - } - - // import node of shape: const { foo } = require('bar') - if ( - isCallExpression(node) && - isLiteral(node.arguments[0]) && - typeof node.arguments[0].value === 'string' - ) { - return node.arguments[0].value; - } - - return undefined; + // import node of shape: import { foo } from 'bar' + if (isImportDeclaration(node) && typeof node.source.value === 'string') { + return node.source.value; + } + + // import node of shape: const { foo } = require('bar') + if ( + isCallExpression(node) && + isLiteral(node.arguments[0]) && + typeof node.arguments[0].value === 'string' + ) { + return node.arguments[0].value; + } + + return undefined; } type AssertNodeInfo = { - matcher: string | null; - isNegated: boolean; + matcher: string | null; + isNegated: boolean; }; /** * Extracts matcher info from MemberExpression node representing an assert. */ export function getAssertNodeInfo( - node: TSESTree.MemberExpression + node: TSESTree.MemberExpression ): AssertNodeInfo { - const emptyInfo = { matcher: null, isNegated: false } as AssertNodeInfo; - - if ( - !isCallExpression(node.object) || - !ASTUtils.isIdentifier(node.object.callee) - ) { - return emptyInfo; - } - - if (node.object.callee.name !== 'expect') { - return emptyInfo; - } - - let matcher = ASTUtils.getPropertyName(node); - const isNegated = matcher === 'not'; - if (isNegated) { - matcher = - node.parent && isMemberExpression(node.parent) - ? ASTUtils.getPropertyName(node.parent) - : null; - } - - if (!matcher) { - return emptyInfo; - } - - return { matcher, isNegated }; + const emptyInfo = { matcher: null, isNegated: false } as AssertNodeInfo; + + if ( + !isCallExpression(node.object) || + !ASTUtils.isIdentifier(node.object.callee) + ) { + return emptyInfo; + } + + if (node.object.callee.name !== 'expect') { + return emptyInfo; + } + + let matcher = ASTUtils.getPropertyName(node); + const isNegated = matcher === 'not'; + if (isNegated) { + matcher = + node.parent && isMemberExpression(node.parent) + ? ASTUtils.getPropertyName(node.parent) + : null; + } + + if (!matcher) { + return emptyInfo; + } + + return { matcher, isNegated }; } +const matcherNamesHandlePromise = [ + // jest matchers + 'resolves', + 'rejects', + // jest-extended matchers + 'toResolve', + 'toReject', + // jasmine matchers + 'toBeRejected', + 'toBeRejectedWith', + 'toBeRejectedWithError', + 'toBePending', + 'toBeResolved', + 'toBeResolvedTo', +]; + /** - * Determines whether a node belongs to an async assertion - * fulfilled by `resolves` or `rejects` properties. - * + * Determines whether a node belongs to an async assertion that is fulfilled by: + * - `resolves` or `rejects` properties + * - `toResolve` or `toReject` jest-extended matchers + * - jasmine async matchers */ -export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean { - if ( - isCallExpression(node) && - ASTUtils.isIdentifier(node.callee) && - node.parent && - isMemberExpression(node.parent) && - node.callee.name === 'expect' - ) { - const expectMatcher = node.parent.property; - return ( - ASTUtils.isIdentifier(expectMatcher) && - (expectMatcher.name === 'resolves' || expectMatcher.name === 'rejects') - ); - } - - if (!node.parent) { - return false; - } - - return hasClosestExpectResolvesRejects(node.parent); +export function hasClosestExpectHandlesPromise(node: TSESTree.Node): boolean { + if ( + isCallExpression(node) && + ASTUtils.isIdentifier(node.callee) && + node.parent && + isMemberExpression(node.parent) && + ['expect', 'expectAsync'].includes(node.callee.name) + ) { + const expectMatcher = node.parent.property; + return ( + ASTUtils.isIdentifier(expectMatcher) && + matcherNamesHandlePromise.includes(expectMatcher.name) + ); + } + + if (!node.parent) { + return false; + } + + return hasClosestExpectHandlesPromise(node.parent); } /** * Gets the Function node which returns the given Identifier. */ export function getInnermostReturningFunction( - context: TSESLint.RuleContext, - node: TSESTree.Identifier + context: TSESLint.RuleContext, + node: TSESTree.Identifier ): - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression - | undefined { - const functionScope = getInnermostFunctionScope(context, node); + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | undefined { + const functionScope = getInnermostFunctionScope(context, node); - if (!functionScope) { - return undefined; - } + if (!functionScope) { + return undefined; + } - const returnStatementNode = getFunctionReturnStatementNode( - functionScope.block - ); + const returnStatementNode = getFunctionReturnStatementNode( + functionScope.block + ); - if (!returnStatementNode) { - return undefined; - } + if (!returnStatementNode) { + return undefined; + } - const returnStatementIdentifier = - getDeepestIdentifierNode(returnStatementNode); + const returnStatementIdentifier = + getDeepestIdentifierNode(returnStatementNode); - if (returnStatementIdentifier?.name !== node.name) { - return undefined; - } + if (returnStatementIdentifier?.name !== node.name) { + return undefined; + } - return functionScope.block; + return functionScope.block; } export function hasImportMatch( - importNode: TSESTree.Identifier | TSESTree.ImportClause, - identifierName: string + importNode: TSESTree.Identifier | TSESTree.ImportClause, + identifierName: string ): boolean { - if (ASTUtils.isIdentifier(importNode)) { - return importNode.name === identifierName; - } + if (ASTUtils.isIdentifier(importNode)) { + return importNode.name === identifierName; + } - return importNode.local.name === identifierName; + return importNode.local.name === identifierName; } export function getStatementCallExpression( - statement: TSESTree.Statement + statement: TSESTree.Statement ): TSESTree.CallExpression | undefined { - if (isExpressionStatement(statement)) { - const { expression } = statement; - if (isCallExpression(expression)) { - return expression; - } - - if ( - ASTUtils.isAwaitExpression(expression) && - isCallExpression(expression.argument) - ) { - return expression.argument; - } - - if (isAssignmentExpression(expression)) { - if (isCallExpression(expression.right)) { - return expression.right; - } - - if ( - ASTUtils.isAwaitExpression(expression.right) && - isCallExpression(expression.right.argument) - ) { - return expression.right.argument; - } - } - } - - if (isReturnStatement(statement) && isCallExpression(statement.argument)) { - return statement.argument; - } - - if (isVariableDeclaration(statement)) { - for (const declaration of statement.declarations) { - if (isCallExpression(declaration.init)) { - return declaration.init; - } - } - } - return undefined; + if (isExpressionStatement(statement)) { + const { expression } = statement; + if (isCallExpression(expression)) { + return expression; + } + + if ( + ASTUtils.isAwaitExpression(expression) && + isCallExpression(expression.argument) + ) { + return expression.argument; + } + + if (isAssignmentExpression(expression)) { + if (isCallExpression(expression.right)) { + return expression.right; + } + + if ( + ASTUtils.isAwaitExpression(expression.right) && + isCallExpression(expression.right.argument) + ) { + return expression.right.argument; + } + } + } + + if (isReturnStatement(statement) && isCallExpression(statement.argument)) { + return statement.argument; + } + + if (isVariableDeclaration(statement)) { + for (const declaration of statement.declarations) { + if (isCallExpression(declaration.init)) { + return declaration.init; + } + } + } + return undefined; } /** @@ -589,60 +665,61 @@ export function getStatementCallExpression( * If node given is not a function, `false` will be returned. */ export function isEmptyFunction(node: TSESTree.Node): boolean | undefined { - if (ASTUtils.isFunction(node) && isBlockStatement(node.body)) { - return node.body.body.length === 0; - } + if (ASTUtils.isFunction(node) && isBlockStatement(node.body)) { + return node.body.body.length === 0; + } - return false; + return false; } /** * Finds the import specifier matching a given name for a given import module node. */ export function findImportSpecifier( - specifierName: string, - node: ImportModuleNode + specifierName: string, + node: ImportModuleNode ): TSESTree.Identifier | TSESTree.ImportClause | undefined { - if (isImportDeclaration(node)) { - const namedExport = node.specifiers.find((n) => { - return ( - isImportSpecifier(n) && - [n.imported.name, n.local.name].includes(specifierName) - ); - }); - - // it is "import { foo [as alias] } from 'baz'" - if (namedExport) { - return namedExport; - } - - // it could be "import * as rtl from 'baz'" - return node.specifiers.find((n) => isImportNamespaceSpecifier(n)); - } else { - if (!ASTUtils.isVariableDeclarator(node.parent)) { - return undefined; - } - const requireNode = node.parent; - - if (ASTUtils.isIdentifier(requireNode.id)) { - // this is const rtl = require('foo') - return requireNode.id; - } - - // this should be const { something } = require('foo') - if (!isObjectPattern(requireNode.id)) { - return undefined; - } - - const property = requireNode.id.properties.find( - (n) => - isProperty(n) && - ASTUtils.isIdentifier(n.key) && - n.key.name === specifierName - ); - if (!property) { - return undefined; - } - return (property as TSESTree.Property).key as TSESTree.Identifier; - } + if (isImportDeclaration(node)) { + const namedExport = node.specifiers.find((n) => { + return ( + isImportSpecifier(n) && + ASTUtils.isIdentifier(n.imported) && + [n.imported.name, n.local.name].includes(specifierName) + ); + }); + + // it is "import { foo [as alias] } from 'baz'" + if (namedExport) { + return namedExport; + } + + // it could be "import * as rtl from 'baz'" + return node.specifiers.find((n) => isImportNamespaceSpecifier(n)); + } else { + if (!ASTUtils.isVariableDeclarator(node.parent)) { + return undefined; + } + const requireNode = node.parent; + + if (ASTUtils.isIdentifier(requireNode.id)) { + // this is const rtl = require('foo') + return requireNode.id; + } + + // this should be const { something } = require('foo') + if (!isObjectPattern(requireNode.id)) { + return undefined; + } + + const property = requireNode.id.properties.find( + (n) => + isProperty(n) && + ASTUtils.isIdentifier(n.key) && + n.key.name === specifierName + ); + if (!property) { + return undefined; + } + return (property as TSESTree.Property).key as TSESTree.Identifier; + } } diff --git a/lib/node-utils/is-node-of-type.ts b/lib/node-utils/is-node-of-type.ts index 1ccddbd4..86e7c881 100644 --- a/lib/node-utils/is-node-of-type.ts +++ b/lib/node-utils/is-node-of-type.ts @@ -1,51 +1,73 @@ -import { - AST_NODE_TYPES, - TSESTree, -} from '@typescript-eslint/experimental-utils'; +import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; -const isNodeOfType = - (nodeType: NodeType) => - ( - node: TSESTree.Node | null | undefined - ): node is TSESTree.Node & { type: NodeType } => - node?.type === nodeType; - -export const isArrayExpression = isNodeOfType(AST_NODE_TYPES.ArrayExpression); -export const isArrowFunctionExpression = isNodeOfType( - AST_NODE_TYPES.ArrowFunctionExpression -); -export const isBlockStatement = isNodeOfType(AST_NODE_TYPES.BlockStatement); -export const isCallExpression = isNodeOfType(AST_NODE_TYPES.CallExpression); -export const isExpressionStatement = isNodeOfType( - AST_NODE_TYPES.ExpressionStatement -); -export const isVariableDeclaration = isNodeOfType( - AST_NODE_TYPES.VariableDeclaration -); -export const isAssignmentExpression = isNodeOfType( - AST_NODE_TYPES.AssignmentExpression -); -export const isSequenceExpression = isNodeOfType( - AST_NODE_TYPES.SequenceExpression -); -export const isImportDeclaration = isNodeOfType( - AST_NODE_TYPES.ImportDeclaration -); -export const isImportDefaultSpecifier = isNodeOfType( - AST_NODE_TYPES.ImportDefaultSpecifier -); -export const isImportNamespaceSpecifier = isNodeOfType( - AST_NODE_TYPES.ImportNamespaceSpecifier -); -export const isImportSpecifier = isNodeOfType(AST_NODE_TYPES.ImportSpecifier); -export const isJSXAttribute = isNodeOfType(AST_NODE_TYPES.JSXAttribute); -export const isLiteral = isNodeOfType(AST_NODE_TYPES.Literal); -export const isMemberExpression = isNodeOfType(AST_NODE_TYPES.MemberExpression); -export const isNewExpression = isNodeOfType(AST_NODE_TYPES.NewExpression); -export const isObjectExpression = isNodeOfType(AST_NODE_TYPES.ObjectExpression); -export const isObjectPattern = isNodeOfType(AST_NODE_TYPES.ObjectPattern); -export const isProperty = isNodeOfType(AST_NODE_TYPES.Property); -export const isReturnStatement = isNodeOfType(AST_NODE_TYPES.ReturnStatement); -export const isFunctionExpression = isNodeOfType( - AST_NODE_TYPES.FunctionExpression +export const isArrayExpression = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ArrayExpression +); +export const isArrowFunctionExpression = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ArrowFunctionExpression +); +export const isBlockStatement = ASTUtils.isNodeOfType( + AST_NODE_TYPES.BlockStatement +); +export const isCallExpression = ASTUtils.isNodeOfType( + AST_NODE_TYPES.CallExpression +); +export const isExpressionStatement = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ExpressionStatement +); +export const isVariableDeclaration = ASTUtils.isNodeOfType( + AST_NODE_TYPES.VariableDeclaration +); +export const isAssignmentExpression = ASTUtils.isNodeOfType( + AST_NODE_TYPES.AssignmentExpression +); +export const isSequenceExpression = ASTUtils.isNodeOfType( + AST_NODE_TYPES.SequenceExpression +); +export const isImportDeclaration = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ImportDeclaration +); +export const isImportDefaultSpecifier = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ImportDefaultSpecifier +); +export const isTSImportEqualsDeclaration = ASTUtils.isNodeOfType( + AST_NODE_TYPES.TSImportEqualsDeclaration +); +export const isImportExpression = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ImportExpression +); +export const isImportNamespaceSpecifier = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ImportNamespaceSpecifier +); +export const isImportSpecifier = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ImportSpecifier +); +export const isJSXAttribute = ASTUtils.isNodeOfType( + AST_NODE_TYPES.JSXAttribute +); +export const isLiteral = ASTUtils.isNodeOfType(AST_NODE_TYPES.Literal); +export const isTemplateLiteral = ASTUtils.isNodeOfType( + AST_NODE_TYPES.TemplateLiteral +); +export const isMemberExpression = ASTUtils.isNodeOfType( + AST_NODE_TYPES.MemberExpression +); +export const isNewExpression = ASTUtils.isNodeOfType( + AST_NODE_TYPES.NewExpression +); +export const isObjectExpression = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ObjectExpression +); +export const isObjectPattern = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ObjectPattern +); +export const isProperty = ASTUtils.isNodeOfType(AST_NODE_TYPES.Property); +export const isReturnStatement = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ReturnStatement +); +export const isFunctionExpression = ASTUtils.isNodeOfType( + AST_NODE_TYPES.FunctionExpression +); +export const isFunctionDeclaration = ASTUtils.isNodeOfType( + AST_NODE_TYPES.FunctionDeclaration ); diff --git a/lib/rules/await-async-events.ts b/lib/rules/await-async-events.ts new file mode 100644 index 00000000..7ac69ede --- /dev/null +++ b/lib/rules/await-async-events.ts @@ -0,0 +1,233 @@ +import { ASTUtils, TSESLint, TSESTree } from '@typescript-eslint/utils'; + +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { + findClosestCallExpressionNode, + findClosestFunctionExpressionNode, + getFunctionName, + getInnermostReturningFunction, + getVariableReferences, + isMemberExpression, + isPromiseHandled, +} from '../node-utils'; +import { EVENTS_SIMULATORS } from '../utils'; + +export const RULE_NAME = 'await-async-events'; +export type MessageIds = 'awaitAsyncEvent' | 'awaitAsyncEventWrapper'; +const FIRE_EVENT_NAME = 'fireEvent'; +const USER_EVENT_NAME = 'userEvent'; +const USER_EVENT_SETUP_FUNCTION_NAME = 'setup'; +type EventModules = (typeof EVENTS_SIMULATORS)[number]; +export type Options = [ + { + eventModule: EventModules | EventModules[]; + }, +]; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Enforce promises from async event methods are handled', + recommendedConfig: { + dom: ['error', { eventModule: 'userEvent' }], + angular: ['error', { eventModule: 'userEvent' }], + react: ['error', { eventModule: 'userEvent' }], + vue: ['error', { eventModule: ['fireEvent', 'userEvent'] }], + svelte: ['error', { eventModule: ['fireEvent', 'userEvent'] }], + marko: ['error', { eventModule: ['fireEvent', 'userEvent'] }], + }, + }, + messages: { + awaitAsyncEvent: + 'Promise returned from async event method `{{ name }}` must be handled', + awaitAsyncEventWrapper: + 'Promise returned from `{{ name }}` wrapper over async event method must be handled', + }, + fixable: 'code', + schema: [ + { + type: 'object', + default: {}, + additionalProperties: false, + properties: { + eventModule: { + default: USER_EVENT_NAME, + oneOf: [ + { + enum: EVENTS_SIMULATORS.concat(), + type: 'string', + }, + { + items: { + type: 'string', + enum: EVENTS_SIMULATORS.concat(), + }, + type: 'array', + }, + ], + }, + }, + }, + ], + }, + defaultOptions: [ + { + eventModule: USER_EVENT_NAME, + }, + ], + + create(context, [options], helpers) { + const functionWrappersNames: string[] = []; + + function reportUnhandledNode({ + node, + closestCallExpression, + messageId = 'awaitAsyncEvent', + fix, + }: { + node: TSESTree.Identifier; + closestCallExpression: TSESTree.CallExpression; + messageId?: MessageIds; + fix?: TSESLint.ReportFixFunction; + }): void { + if (!isPromiseHandled(node)) { + context.report({ + node: closestCallExpression.callee, + messageId, + data: { name: node.name }, + fix, + }); + } + } + + function detectEventMethodWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + functionWrappersNames.push(getFunctionName(innerFunction)); + } + } + + const eventModules = + typeof options.eventModule === 'string' + ? [options.eventModule] + : options.eventModule; + const isFireEventEnabled = eventModules.includes(FIRE_EVENT_NAME); + const isUserEventEnabled = eventModules.includes(USER_EVENT_NAME); + + return { + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if ( + (isFireEventEnabled && helpers.isFireEventMethod(node)) || + (isUserEventEnabled && helpers.isUserEventMethod(node)) + ) { + if (node.name === USER_EVENT_SETUP_FUNCTION_NAME) { + return; + } + + detectEventMethodWrapper(node); + + const closestCallExpression = findClosestCallExpressionNode( + node, + true + ); + + if (!closestCallExpression?.parent) { + return; + } + + const references = getVariableReferences( + context, + closestCallExpression.parent + ); + + if (references.length === 0) { + reportUnhandledNode({ + node, + closestCallExpression, + fix: (fixer) => { + if (isMemberExpression(node.parent)) { + const functionExpression = + findClosestFunctionExpressionNode(node); + + if (functionExpression) { + const memberExpressionFixer = fixer.insertTextBefore( + node.parent, + 'await ' + ); + + if (functionExpression.async) { + return memberExpressionFixer; + } else { + // Mutate the actual node so if other nodes exist in this + // function expression body they don't also try to fix it. + functionExpression.async = true; + + return [ + memberExpressionFixer, + fixer.insertTextBefore(functionExpression, 'async '), + ]; + } + } + } + + return null; + }, + }); + } else { + for (const reference of references) { + if (ASTUtils.isIdentifier(reference.identifier)) { + reportUnhandledNode({ + node: reference.identifier, + closestCallExpression, + }); + } + } + } + } else if (functionWrappersNames.includes(node.name)) { + // report promise returned from function wrapping fire event method + // previously detected + const closestCallExpression = findClosestCallExpressionNode( + node, + true + ); + + if (!closestCallExpression) { + return; + } + + reportUnhandledNode({ + node, + closestCallExpression, + messageId: 'awaitAsyncEventWrapper', + fix: (fixer) => { + const functionExpression = + findClosestFunctionExpressionNode(node); + + if (functionExpression) { + const nodeFixer = fixer.insertTextBefore(node, 'await '); + + if (functionExpression.async) { + return nodeFixer; + } else { + // Mutate the actual node so if other nodes exist in this + // function expression body they don't also try to fix it. + functionExpression.async = true; + + return [ + nodeFixer, + fixer.insertTextBefore(functionExpression, 'async '), + ]; + } + } + + return null; + }, + }); + } + }, + }; + }, +}); diff --git a/lib/rules/await-async-queries.ts b/lib/rules/await-async-queries.ts new file mode 100644 index 00000000..a2befb2a --- /dev/null +++ b/lib/rules/await-async-queries.ts @@ -0,0 +1,121 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; + +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { + findClosestCallExpressionNode, + getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, + getVariableReferences, + isPromiseHandled, +} from '../node-utils'; + +export const RULE_NAME = 'await-async-queries'; +export type MessageIds = 'asyncQueryWrapper' | 'awaitAsyncQuery'; +type Options = []; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Enforce promises from async queries to be handled', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + awaitAsyncQuery: + 'promise returned from `{{ name }}` query must be handled', + asyncQueryWrapper: + 'promise returned from `{{ name }}` wrapper over async query must be handled', + }, + schema: [], + }, + defaultOptions: [], + + create(context, _, helpers) { + const functionWrappersNames: string[] = []; + + function detectAsyncQueryWrapper(node: TSESTree.Identifier) { + const innerFunction = getInnermostReturningFunction(context, node); + if (innerFunction) { + functionWrappersNames.push(getFunctionName(innerFunction)); + } + } + + return { + CallExpression(node) { + const identifierNode = getDeepestIdentifierNode(node); + + if (!identifierNode) { + return; + } + + if (helpers.isAsyncQuery(identifierNode)) { + // detect async query used within wrapper function for later analysis + detectAsyncQueryWrapper(identifierNode); + + const closestCallExpressionNode = findClosestCallExpressionNode( + node, + true + ); + + if (!closestCallExpressionNode?.parent) { + return; + } + + const references = getVariableReferences( + context, + closestCallExpressionNode.parent + ); + + // check direct usage of async query: + // const element = await findByRole('button') + if (references.length === 0) { + if (!isPromiseHandled(identifierNode)) { + context.report({ + node: identifierNode, + messageId: 'awaitAsyncQuery', + data: { name: identifierNode.name }, + }); + return; + } + } + + // check references usages of async query: + // const promise = findByRole('button') + // const element = await promise + for (const reference of references) { + if ( + ASTUtils.isIdentifier(reference.identifier) && + !isPromiseHandled(reference.identifier) + ) { + context.report({ + node: identifierNode, + messageId: 'awaitAsyncQuery', + data: { name: identifierNode.name }, + }); + return; + } + } + } else if ( + functionWrappersNames.includes(identifierNode.name) && + !isPromiseHandled(identifierNode) + ) { + // check async queries used within a wrapper previously detected + context.report({ + node: identifierNode, + messageId: 'asyncQueryWrapper', + data: { name: identifierNode.name }, + }); + } + }, + }; + }, +}); diff --git a/lib/rules/await-async-query.ts b/lib/rules/await-async-query.ts deleted file mode 100644 index 90be4e9f..00000000 --- a/lib/rules/await-async-query.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; - -import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { - findClosestCallExpressionNode, - getDeepestIdentifierNode, - getFunctionName, - getInnermostReturningFunction, - getVariableReferences, - isPromiseHandled, -} from '../node-utils'; - -export const RULE_NAME = 'await-async-query'; -export type MessageIds = 'asyncQueryWrapper' | 'awaitAsyncQuery'; -type Options = []; - -export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: 'Enforce promises from async queries to be handled', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - awaitAsyncQuery: - 'promise returned from `{{ name }}` query must be handled', - asyncQueryWrapper: - 'promise returned from `{{ name }}` wrapper over async query must be handled', - }, - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - const functionWrappersNames: string[] = []; - - function detectAsyncQueryWrapper(node: TSESTree.Identifier) { - const innerFunction = getInnermostReturningFunction(context, node); - if (innerFunction) { - functionWrappersNames.push(getFunctionName(innerFunction)); - } - } - - return { - CallExpression(node) { - const identifierNode = getDeepestIdentifierNode(node); - - if (!identifierNode) { - return; - } - - if (helpers.isAsyncQuery(identifierNode)) { - // detect async query used within wrapper function for later analysis - detectAsyncQueryWrapper(identifierNode); - - const closestCallExpressionNode = findClosestCallExpressionNode( - node, - true - ); - - if (!closestCallExpressionNode || !closestCallExpressionNode.parent) { - return; - } - - const references = getVariableReferences( - context, - closestCallExpressionNode.parent - ); - - // check direct usage of async query: - // const element = await findByRole('button') - if (references.length === 0) { - if (!isPromiseHandled(identifierNode)) { - context.report({ - node: identifierNode, - messageId: 'awaitAsyncQuery', - data: { name: identifierNode.name }, - }); - return; - } - } - - // check references usages of async query: - // const promise = findByRole('button') - // const element = await promise - for (const reference of references) { - if ( - ASTUtils.isIdentifier(reference.identifier) && - !isPromiseHandled(reference.identifier) - ) { - context.report({ - node: identifierNode, - messageId: 'awaitAsyncQuery', - data: { name: identifierNode.name }, - }); - return; - } - } - } else if ( - functionWrappersNames.includes(identifierNode.name) && - !isPromiseHandled(identifierNode) - ) { - // check async queries used within a wrapper previously detected - context.report({ - node: identifierNode, - messageId: 'asyncQueryWrapper', - data: { name: identifierNode.name }, - }); - } - }, - }; - }, -}); diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 1f23e9fa..56d2223b 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -1,12 +1,16 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { TSESTree, ASTUtils } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - findClosestCallExpressionNode, - getFunctionName, - getInnermostReturningFunction, - getVariableReferences, - isPromiseHandled, + findClosestCallExpressionNode, + getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, + getVariableReferences, + isCallExpression, + isObjectPattern, + isPromiseHandled, + isProperty, } from '../node-utils'; export const RULE_NAME = 'await-async-utils'; @@ -14,94 +18,168 @@ export type MessageIds = 'asyncUtilWrapper' | 'awaitAsyncUtil'; type Options = []; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: 'Enforce promises from async utils to be awaited properly', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - awaitAsyncUtil: 'Promise returned from `{{ name }}` must be handled', - asyncUtilWrapper: - 'Promise returned from {{ name }} wrapper over async util must be handled', - }, - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - const functionWrappersNames: string[] = []; - - function detectAsyncUtilWrapper(node: TSESTree.Identifier) { - const innerFunction = getInnermostReturningFunction(context, node); - - if (innerFunction) { - functionWrappersNames.push(getFunctionName(innerFunction)); - } - } - - return { - 'CallExpression Identifier'(node: TSESTree.Identifier) { - if (helpers.isAsyncUtil(node)) { - // detect async query used within wrapper function for later analysis - detectAsyncUtilWrapper(node); - - const closestCallExpression = findClosestCallExpressionNode( - node, - true - ); - - if (!closestCallExpression || !closestCallExpression.parent) { - return; - } - - const references = getVariableReferences( - context, - closestCallExpression.parent - ); - - if (references.length === 0) { - if (!isPromiseHandled(node)) { - context.report({ - node, - messageId: 'awaitAsyncUtil', - data: { - name: node.name, - }, - }); - } - } else { - for (const reference of references) { - const referenceNode = reference.identifier as TSESTree.Identifier; - if (!isPromiseHandled(referenceNode)) { - context.report({ - node, - messageId: 'awaitAsyncUtil', - data: { - name: node.name, - }, - }); - return; - } - } - } - } else if (functionWrappersNames.includes(node.name)) { - // check async queries used within a wrapper previously detected - if (!isPromiseHandled(node)) { - context.report({ - node, - messageId: 'asyncUtilWrapper', - data: { name: node.name }, - }); - } - } - }, - }; - }, + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Enforce promises from async utils to be awaited properly', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + awaitAsyncUtil: 'Promise returned from `{{ name }}` must be handled', + asyncUtilWrapper: + 'Promise returned from {{ name }} wrapper over async util must be handled', + }, + schema: [], + }, + defaultOptions: [], + + create(context, _, helpers) { + const functionWrappersNames: string[] = []; + + function detectAsyncUtilWrapper(node: TSESTree.Identifier) { + const innerFunction = getInnermostReturningFunction(context, node); + if (!innerFunction) { + return; + } + + const functionName = getFunctionName(innerFunction); + if (functionName.length === 0) { + return; + } + + functionWrappersNames.push(functionName); + } + + /* + Example: + `const { myAsyncWrapper: myRenamedValue } = someObject`; + Detects `myRenamedValue` and adds it to the known async wrapper names. + */ + function detectDestructuredAsyncUtilWrapperAliases( + node: TSESTree.ObjectPattern + ) { + for (const property of node.properties) { + if (!isProperty(property)) { + continue; + } + + if ( + !ASTUtils.isIdentifier(property.key) || + !ASTUtils.isIdentifier(property.value) + ) { + continue; + } + + if (functionWrappersNames.includes(property.key.name)) { + const isDestructuredAsyncWrapperPropertyRenamed = + property.key.name !== property.value.name; + + if (isDestructuredAsyncWrapperPropertyRenamed) { + functionWrappersNames.push(property.value.name); + } + } + } + } + + /* + Either we report a direct usage of an async util or a usage of a wrapper + around an async util + */ + const getMessageId = (node: TSESTree.Identifier): MessageIds => { + if (helpers.isAsyncUtil(node)) { + return 'awaitAsyncUtil'; + } + + return 'asyncUtilWrapper'; + }; + + return { + VariableDeclarator(node: TSESTree.VariableDeclarator) { + if (isObjectPattern(node.id)) { + detectDestructuredAsyncUtilWrapperAliases(node.id); + return; + } + + const isAssigningKnownAsyncFunctionWrapper = + ASTUtils.isIdentifier(node.id) && + node.init !== null && + !isCallExpression(node.init) && + !ASTUtils.isAwaitExpression(node.init) && + functionWrappersNames.includes( + getDeepestIdentifierNode(node.init)?.name ?? '' + ); + + if (isAssigningKnownAsyncFunctionWrapper) { + functionWrappersNames.push((node.id as TSESTree.Identifier).name); + } + }, + CallExpression(node: TSESTree.CallExpression) { + const callExpressionIdentifier = getDeepestIdentifierNode(node); + + if (!callExpressionIdentifier) { + return; + } + + const isAsyncUtilOrKnownAliasAroundIt = + helpers.isAsyncUtil(callExpressionIdentifier) || + functionWrappersNames.includes(callExpressionIdentifier.name); + if (!isAsyncUtilOrKnownAliasAroundIt) { + return; + } + + // detect async query used within wrapper function for later analysis + if (helpers.isAsyncUtil(callExpressionIdentifier)) { + detectAsyncUtilWrapper(callExpressionIdentifier); + } + + const closestCallExpression = findClosestCallExpressionNode( + callExpressionIdentifier, + true + ); + + if (!closestCallExpression?.parent) { + return; + } + + const references = getVariableReferences( + context, + closestCallExpression.parent + ); + + if (references.length === 0) { + if (!isPromiseHandled(callExpressionIdentifier)) { + context.report({ + node: callExpressionIdentifier, + messageId: getMessageId(callExpressionIdentifier), + data: { + name: callExpressionIdentifier.name, + }, + }); + } + } else { + for (const reference of references) { + const referenceNode = reference.identifier as TSESTree.Identifier; + if (!isPromiseHandled(referenceNode)) { + context.report({ + node: callExpressionIdentifier, + messageId: getMessageId(callExpressionIdentifier), + data: { + name: callExpressionIdentifier.name, + }, + }); + return; + } + } + } + }, + }; + }, }); diff --git a/lib/rules/await-fire-event.ts b/lib/rules/await-fire-event.ts deleted file mode 100644 index e6a70568..00000000 --- a/lib/rules/await-fire-event.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; - -import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { - findClosestCallExpressionNode, - getFunctionName, - getInnermostReturningFunction, - getVariableReferences, - isPromiseHandled, -} from '../node-utils'; - -export const RULE_NAME = 'await-fire-event'; -export type MessageIds = 'awaitFireEvent' | 'fireEventWrapper'; -type Options = []; - -export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: 'Enforce promises from `fireEvent` methods to be handled', - recommendedConfig: { - dom: false, - angular: false, - react: false, - vue: 'error', - }, - }, - messages: { - awaitFireEvent: - 'Promise returned from `fireEvent.{{ methodName }}` must be handled', - fireEventWrapper: - 'Promise returned from `fireEvent.{{ wrapperName }}` wrapper over fire event method must be handled', - }, - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - const functionWrappersNames: string[] = []; - - function reportUnhandledNode( - node: TSESTree.Identifier, - closestCallExpressionNode: TSESTree.CallExpression, - messageId: MessageIds = 'awaitFireEvent' - ): void { - if (!isPromiseHandled(node)) { - context.report({ - node: closestCallExpressionNode.callee, - messageId, - data: { name: node.name }, - }); - } - } - - function detectFireEventMethodWrapper(node: TSESTree.Identifier): void { - const innerFunction = getInnermostReturningFunction(context, node); - - if (innerFunction) { - functionWrappersNames.push(getFunctionName(innerFunction)); - } - } - - return { - 'CallExpression Identifier'(node: TSESTree.Identifier) { - if (helpers.isFireEventMethod(node)) { - detectFireEventMethodWrapper(node); - - const closestCallExpression = findClosestCallExpressionNode( - node, - true - ); - - if (!closestCallExpression || !closestCallExpression.parent) { - return; - } - - const references = getVariableReferences( - context, - closestCallExpression.parent - ); - - if (references.length === 0) { - reportUnhandledNode(node, closestCallExpression); - } else { - for (const reference of references) { - if (ASTUtils.isIdentifier(reference.identifier)) { - reportUnhandledNode( - reference.identifier, - closestCallExpression - ); - } - } - } - } else if (functionWrappersNames.includes(node.name)) { - // report promise returned from function wrapping fire event method - // previously detected - const closestCallExpression = findClosestCallExpressionNode( - node, - true - ); - - if (!closestCallExpression) { - return; - } - - reportUnhandledNode(node, closestCallExpression, 'fireEventWrapper'); - } - }, - }; - }, -}); diff --git a/lib/rules/consistent-data-testid.ts b/lib/rules/consistent-data-testid.ts index ed462367..f6d32611 100644 --- a/lib/rules/consistent-data-testid.ts +++ b/lib/rules/consistent-data-testid.ts @@ -1,125 +1,151 @@ import { createTestingLibraryRule } from '../create-testing-library-rule'; import { isJSXAttribute, isLiteral } from '../node-utils'; +import { getFilename } from '../utils'; export const RULE_NAME = 'consistent-data-testid'; -export type MessageIds = 'consistentDataTestId'; +export type MessageIds = + | 'consistentDataTestId' + | 'consistentDataTestIdCustomMessage'; export type Options = [ - { - testIdAttribute?: string[] | string; - testIdPattern: string; - } + { + testIdAttribute?: string[] | string; + testIdPattern: string; + customMessage?: string; + }, ]; const FILENAME_PLACEHOLDER = '{fileName}'; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: 'Ensures consistent usage of `data-testid`', - recommendedConfig: { - dom: false, - angular: false, - react: false, - vue: false, - }, - }, - messages: { - consistentDataTestId: '`{{attr}}` "{{value}}" should match `{{regex}}`', - }, - schema: [ - { - type: 'object', - default: {}, - additionalProperties: false, - required: ['testIdPattern'], - properties: { - testIdPattern: { - type: 'string', - }, - testIdAttribute: { - default: 'data-testid', - oneOf: [ - { - type: 'string', - }, - { - type: 'array', - items: { - type: 'string', - }, - }, - ], - }, - }, - }, - ], - }, - defaultOptions: [ - { - testIdPattern: '', - testIdAttribute: 'data-testid', - }, - ], - detectionOptions: { - skipRuleReportingCheck: true, - }, + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: 'Ensures consistent usage of `data-testid`', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + svelte: false, + marko: false, + }, + }, + messages: { + consistentDataTestId: '`{{attr}}` "{{value}}" should match `{{regex}}`', + consistentDataTestIdCustomMessage: '`{{message}}`', + }, + schema: [ + { + type: 'object', + default: {}, + additionalProperties: false, + required: ['testIdPattern'], + properties: { + testIdPattern: { + type: 'string', + }, + testIdAttribute: { + default: 'data-testid', + oneOf: [ + { + type: 'string', + }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], + }, + customMessage: { + default: undefined, + type: 'string', + }, + }, + }, + ], + }, + defaultOptions: [ + { + testIdPattern: '', + testIdAttribute: 'data-testid', + customMessage: undefined, + }, + ], + detectionOptions: { + skipRuleReportingCheck: true, + }, - create: (context, [options]) => { - const { getFilename } = context; - const { testIdPattern, testIdAttribute: attr } = options; + create: (context, [options]) => { + const { testIdPattern, testIdAttribute: attr, customMessage } = options; - function getFileNameData() { - const splitPath = getFilename().split('/'); - const fileNameWithExtension = splitPath.pop() ?? ''; - const parent = splitPath.pop(); - const fileName = fileNameWithExtension.split('.').shift(); + function getFileNameData() { + const splitPath = getFilename(context).split('/'); + const fileNameWithExtension = splitPath.pop() ?? ''; + if ( + fileNameWithExtension.includes('[') || + fileNameWithExtension.includes(']') + ) { + return { fileName: undefined }; + } + const parent = splitPath.pop(); + const fileName = fileNameWithExtension.split('.').shift(); - return { - fileName: fileName === 'index' ? parent : fileName, - }; - } + return { + fileName: fileName === 'index' ? parent : fileName, + }; + } - function getTestIdValidator(fileName: string) { - return new RegExp(testIdPattern.replace(FILENAME_PLACEHOLDER, fileName)); - } + function getTestIdValidator(fileName: string) { + return new RegExp(testIdPattern.replace(FILENAME_PLACEHOLDER, fileName)); + } - function isTestIdAttribute(name: string): boolean { - if (typeof attr === 'string') { - return attr === name; - } else { - return attr?.includes(name) ?? false; - } - } + function isTestIdAttribute(name: string): boolean { + if (typeof attr === 'string') { + return attr === name; + } else { + return attr?.includes(name) ?? false; + } + } - return { - JSXIdentifier: (node) => { - if ( - !node.parent || - !isJSXAttribute(node.parent) || - !isLiteral(node.parent.value) || - !isTestIdAttribute(node.name) - ) { - return; - } + function getErrorMessageId(): MessageIds { + if (customMessage === undefined) { + return 'consistentDataTestId'; + } - const value = node.parent.value.value; - const { fileName } = getFileNameData(); - const regex = getTestIdValidator(fileName ?? ''); + return 'consistentDataTestIdCustomMessage'; + } - if (value && typeof value === 'string' && !regex.test(value)) { - context.report({ - node, - messageId: 'consistentDataTestId', - data: { - attr: node.name, - value, - regex, - }, - }); - } - }, - }; - }, + return { + JSXIdentifier: (node) => { + if ( + !node.parent || + !isJSXAttribute(node.parent) || + !isLiteral(node.parent.value) || + !isTestIdAttribute(node.name) + ) { + return; + } + + const value = node.parent.value.value; + const { fileName } = getFileNameData(); + const regex = getTestIdValidator(fileName ?? ''); + + if (value && typeof value === 'string' && !regex.test(value)) { + context.report({ + node, + messageId: getErrorMessageId(), + data: { + attr: node.name, + value, + regex, + message: customMessage, + }, + }); + } + }, + }; + }, }); diff --git a/lib/rules/index.ts b/lib/rules/index.ts index 1a1e79f1..94dc6754 100644 --- a/lib/rules/index.ts +++ b/lib/rules/index.ts @@ -1,26 +1,20 @@ import { readdirSync } from 'fs'; import { join, parse } from 'path'; -import { TSESLint } from '@typescript-eslint/experimental-utils'; - -import { importDefault, TestingLibraryRuleMeta } from '../utils'; - -type RuleModule = TSESLint.RuleModule & { - meta: TestingLibraryRuleMeta & { - recommended: false; - }; -}; +import { importDefault, TestingLibraryPluginRuleModule } from '../utils'; const rulesDir = __dirname; const excludedFiles = ['index']; export default readdirSync(rulesDir) - .map((rule) => parse(rule).name) - .filter((ruleName) => !excludedFiles.includes(ruleName)) - .reduce>( - (allRules, ruleName) => ({ - ...allRules, - [ruleName]: importDefault(join(rulesDir, ruleName)), - }), - {} - ); + .map((rule) => parse(rule).name) + .filter((ruleName) => !excludedFiles.includes(ruleName)) + .reduce>>( + (allRules, ruleName) => ({ + ...allRules, + [ruleName]: importDefault< + TestingLibraryPluginRuleModule + >(join(rulesDir, ruleName)), + }), + {} + ); diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index 87753dc6..5d563571 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -1,96 +1,176 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - getDeepestIdentifierNode, - getPropertyIdentifierNode, - isLiteral, - isObjectExpression, - isProperty, + getDeepestIdentifierNode, + getPropertyIdentifierNode, + isLiteral, + isObjectExpression, + isProperty, } from '../node-utils'; +const USER_EVENT_ASYNC_EXCEPTIONS = ['type', 'keyboard']; +const FIRE_EVENT_OPTION = 'fire-event'; +const USER_EVENT_OPTION = 'user-event'; +const VALID_EVENT_MODULES = [FIRE_EVENT_OPTION, USER_EVENT_OPTION]; +const DEFAULT_EVENT_MODULES = [FIRE_EVENT_OPTION]; + export const RULE_NAME = 'no-await-sync-events'; export type MessageIds = 'noAwaitSyncEvents'; -type Options = []; -const USER_EVENT_ASYNC_EXCEPTIONS: string[] = ['type', 'keyboard']; +type ValidEventModules = (typeof VALID_EVENT_MODULES)[number]; +type EventModulesOptions = ReadonlyArray; +type Options = [{ eventModules?: EventModulesOptions }]; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: 'Disallow unnecessary `await` for sync events', - recommendedConfig: { - dom: false, - angular: false, - react: false, - vue: false, - }, - }, - messages: { - noAwaitSyncEvents: - '`{{ name }}` is sync and does not need `await` operator', - }, - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - // userEvent.type() and userEvent.keyboard() are exceptions, which returns a - // Promise. But it is only necessary to wait when delay option other than 0 - // is specified. So this rule has a special exception for the case await: - // - userEvent.type(element, 'abc', {delay: 1234}) - // - userEvent.keyboard('abc', {delay: 1234}) - return { - 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { - const simulateEventFunctionIdentifier = getDeepestIdentifierNode(node); - - if (!simulateEventFunctionIdentifier) { - return; - } - - const isSimulateEventMethod = - helpers.isUserEventMethod(simulateEventFunctionIdentifier) || - helpers.isFireEventMethod(simulateEventFunctionIdentifier); - - if (!isSimulateEventMethod) { - return; - } - - const lastArg = node.arguments[node.arguments.length - 1]; - - const hasDelay = - isObjectExpression(lastArg) && - lastArg.properties.some( - (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - property.key.name === 'delay' && - isLiteral(property.value) && - !!property.value.value && - property.value.value > 0 - ); - - const simulateEventFunctionName = simulateEventFunctionIdentifier.name; - - if ( - USER_EVENT_ASYNC_EXCEPTIONS.includes(simulateEventFunctionName) && - hasDelay - ) { - return; - } - - context.report({ - node, - messageId: 'noAwaitSyncEvents', - data: { - name: `${ - getPropertyIdentifierNode(node)?.name - }.${simulateEventFunctionName}`, - }, - }); - }, - }; - }, + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Disallow unnecessary `await` for sync events', + recommendedConfig: { + dom: ['error', { eventModules: DEFAULT_EVENT_MODULES }], + angular: ['error', { eventModules: DEFAULT_EVENT_MODULES }], + react: ['error', { eventModules: DEFAULT_EVENT_MODULES }], + vue: false, + svelte: false, + marko: false, + }, + }, + messages: { + noAwaitSyncEvents: + '`{{ name }}` is sync and does not need `await` operator', + }, + schema: [ + { + type: 'object', + properties: { + eventModules: { + type: 'array', + items: { type: 'string', enum: VALID_EVENT_MODULES }, + minItems: 1, + default: DEFAULT_EVENT_MODULES, + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [{ eventModules: DEFAULT_EVENT_MODULES }], + + create(context, [options], helpers) { + const { eventModules = DEFAULT_EVENT_MODULES } = options; + let hasDelayDeclarationOrAssignmentGTZero: boolean; + + // userEvent.type() and userEvent.keyboard() are exceptions, which returns a + // Promise. But it is only necessary to wait when delay option other than 0 + // is specified. So this rule has a special exception for the case await: + // - userEvent.type(element, 'abc', {delay: 1234}) + // - userEvent.keyboard('abc', {delay: 1234}) + return { + VariableDeclaration(node: TSESTree.VariableDeclaration) { + // Case delay has been declared outside of call expression's arguments + // Let's save the info if it is greater than zero + hasDelayDeclarationOrAssignmentGTZero = node.declarations.some( + (property) => + ASTUtils.isIdentifier(property.id) && + property.id.name === 'delay' && + isLiteral(property.init) && + property.init.value && + Number.isInteger(property.init.value) && + Number(property.init.value) > 0 + ); + }, + AssignmentExpression(node: TSESTree.AssignmentExpression) { + // Case delay has been assigned or re-assigned outside of call expression's arguments + // Let's save the info if it is greater than zero + if ( + ASTUtils.isIdentifier(node.left) && + node.left.name === 'delay' && + isLiteral(node.right) && + node.right.value !== null + ) { + hasDelayDeclarationOrAssignmentGTZero = + Number.isInteger(node.right.value) && Number(node.right.value) > 0; + } + }, + 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { + const simulateEventFunctionIdentifier = getDeepestIdentifierNode(node); + + if (!simulateEventFunctionIdentifier) { + return; + } + + const isUserEventMethod = helpers.isUserEventMethod( + simulateEventFunctionIdentifier + ); + const isFireEventMethod = helpers.isFireEventMethod( + simulateEventFunctionIdentifier + ); + const isSimulateEventMethod = isUserEventMethod || isFireEventMethod; + + if (!isSimulateEventMethod) { + return; + } + + if (isFireEventMethod && !eventModules.includes(FIRE_EVENT_OPTION)) { + return; + } + if (isUserEventMethod && !eventModules.includes(USER_EVENT_OPTION)) { + return; + } + + const lastArg = node.arguments[node.arguments.length - 1]; + + // Checking if there's a delay property + // Note: delay's value may have declared or assigned somewhere else (as a variable declaration or as an assignment expression) + // or right after this (as a literal) + const hasDelayProperty = + isObjectExpression(lastArg) && + lastArg.properties.some( + (property) => + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + property.key.name === 'delay' + ); + + // In case delay's value has been declared as a literal + const hasDelayLiteralGTZero = + isObjectExpression(lastArg) && + lastArg.properties.some( + (property) => + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + property.key.name === 'delay' && + isLiteral(property.value) && + !!property.value.value && + Number.isInteger(property.value.value) && + Number(property.value.value) > 0 + ); + + const simulateEventFunctionName = simulateEventFunctionIdentifier.name; + + if ( + USER_EVENT_ASYNC_EXCEPTIONS.includes(simulateEventFunctionName) && + hasDelayProperty && + (hasDelayDeclarationOrAssignmentGTZero || hasDelayLiteralGTZero) + ) { + return; + } + + const eventModuleName = getPropertyIdentifierNode(node)?.name; + const eventFullName = eventModuleName + ? `${eventModuleName}.${simulateEventFunctionName}` + : simulateEventFunctionName; + + context.report({ + node, + messageId: 'noAwaitSyncEvents', + data: { + name: eventFullName, + }, + }); + }, + }; + }, }); diff --git a/lib/rules/no-await-sync-queries.ts b/lib/rules/no-await-sync-queries.ts new file mode 100644 index 00000000..b9c3414c --- /dev/null +++ b/lib/rules/no-await-sync-queries.ts @@ -0,0 +1,54 @@ +import { TSESTree } from '@typescript-eslint/utils'; + +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { getDeepestIdentifierNode } from '../node-utils'; + +export const RULE_NAME = 'no-await-sync-queries'; +export type MessageIds = 'noAwaitSyncQuery'; +type Options = []; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Disallow unnecessary `await` for sync queries', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + noAwaitSyncQuery: + '`{{ name }}` query is sync so it does not need to be awaited', + }, + schema: [], + }, + defaultOptions: [], + + create(context, _, helpers) { + return { + 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { + const deepestIdentifierNode = getDeepestIdentifierNode(node); + + if (!deepestIdentifierNode) { + return; + } + + if (helpers.isSyncQuery(deepestIdentifierNode)) { + context.report({ + node: deepestIdentifierNode, + messageId: 'noAwaitSyncQuery', + data: { + name: deepestIdentifierNode.name, + }, + }); + } + }, + }; + }, +}); diff --git a/lib/rules/no-await-sync-query.ts b/lib/rules/no-await-sync-query.ts deleted file mode 100644 index 1797aa9d..00000000 --- a/lib/rules/no-await-sync-query.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; - -import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { getDeepestIdentifierNode } from '../node-utils'; - -export const RULE_NAME = 'no-await-sync-query'; -export type MessageIds = 'noAwaitSyncQuery'; -type Options = []; - -export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: 'Disallow unnecessary `await` for sync queries', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - noAwaitSyncQuery: - '`{{ name }}` query is sync so it does not need to be awaited', - }, - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - return { - 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { - const deepestIdentifierNode = getDeepestIdentifierNode(node); - - if (!deepestIdentifierNode) { - return; - } - - if (helpers.isSyncQuery(deepestIdentifierNode)) { - context.report({ - node: deepestIdentifierNode, - messageId: 'noAwaitSyncQuery', - data: { - name: deepestIdentifierNode.name, - }, - }); - } - }, - }; - }, -}); diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index e64af6ca..a3614cf9 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -1,13 +1,13 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - getDeepestIdentifierNode, - getFunctionName, - getInnermostReturningFunction, - isMemberExpression, - isObjectPattern, - isProperty, + getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, + isMemberExpression, + isObjectPattern, + isProperty, } from '../node-utils'; export const RULE_NAME = 'no-container'; @@ -15,151 +15,153 @@ export type MessageIds = 'noContainer'; type Options = []; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: 'Disallow the use of `container` methods', - recommendedConfig: { - dom: false, - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - noContainer: - 'Avoid using container methods. Prefer using the methods from Testing Library, such as "getByRole()"', - }, - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - const destructuredContainerPropNames: string[] = []; - const renderWrapperNames: string[] = []; - let renderResultVarName: string | null = null; - let containerName: string | null = null; - let containerCallsMethod = false; - - function detectRenderWrapper(node: TSESTree.Identifier): void { - const innerFunction = getInnermostReturningFunction(context, node); - - if (innerFunction) { - renderWrapperNames.push(getFunctionName(innerFunction)); - } - } - - function showErrorIfChainedContainerMethod( - innerNode: TSESTree.MemberExpression - ) { - if (isMemberExpression(innerNode)) { - if (ASTUtils.isIdentifier(innerNode.object)) { - const isContainerName = innerNode.object.name === containerName; - - if (isContainerName) { - context.report({ - node: innerNode, - messageId: 'noContainer', - }); - return; - } - - const isRenderWrapper = innerNode.object.name === renderResultVarName; - containerCallsMethod = - ASTUtils.isIdentifier(innerNode.property) && - innerNode.property.name === 'container' && - isRenderWrapper; - - if (containerCallsMethod) { - context.report({ - node: innerNode.property, - messageId: 'noContainer', - }); - return; - } - } - showErrorIfChainedContainerMethod( - innerNode.object as TSESTree.MemberExpression - ); - } - } - - return { - CallExpression(node) { - const callExpressionIdentifier = getDeepestIdentifierNode(node); - - if (!callExpressionIdentifier) { - return; - } - - if (helpers.isRenderUtil(callExpressionIdentifier)) { - detectRenderWrapper(callExpressionIdentifier); - } - - if (isMemberExpression(node.callee)) { - showErrorIfChainedContainerMethod(node.callee); - } else if ( - ASTUtils.isIdentifier(node.callee) && - destructuredContainerPropNames.includes(node.callee.name) - ) { - context.report({ - node, - messageId: 'noContainer', - }); - } - }, - - VariableDeclarator(node) { - if (!node.init) { - return; - } - const initIdentifierNode = getDeepestIdentifierNode(node.init); - - if (!initIdentifierNode) { - return; - } - - const isRenderWrapperVariableDeclarator = renderWrapperNames.includes( - initIdentifierNode.name - ); - - if ( - !helpers.isRenderVariableDeclarator(node) && - !isRenderWrapperVariableDeclarator - ) { - return; - } - - if (isObjectPattern(node.id)) { - const containerIndex = node.id.properties.findIndex( - (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - property.key.name === 'container' - ); - - const nodeValue = - containerIndex !== -1 && node.id.properties[containerIndex].value; - - if (!nodeValue) { - return; - } - - if (ASTUtils.isIdentifier(nodeValue)) { - containerName = nodeValue.name; - } else if (isObjectPattern(nodeValue)) { - nodeValue.properties.forEach( - (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - destructuredContainerPropNames.push(property.key.name) - ); - } - } else if (ASTUtils.isIdentifier(node.id)) { - renderResultVarName = node.id.name; - } - }, - }; - }, + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Disallow the use of `container` methods', + recommendedConfig: { + dom: false, + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + noContainer: + 'Avoid using container methods. Prefer using the methods from Testing Library, such as "getByRole()"', + }, + schema: [], + }, + defaultOptions: [], + + create(context, _, helpers) { + const destructuredContainerPropNames: string[] = []; + const renderWrapperNames: string[] = []; + let renderResultVarName: string | null = null; + let containerName: string | null = null; + let containerCallsMethod = false; + + function detectRenderWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + renderWrapperNames.push(getFunctionName(innerFunction)); + } + } + + function showErrorIfChainedContainerMethod( + innerNode: TSESTree.MemberExpression + ) { + if (isMemberExpression(innerNode)) { + if (ASTUtils.isIdentifier(innerNode.object)) { + const isContainerName = innerNode.object.name === containerName; + + if (isContainerName) { + context.report({ + node: innerNode, + messageId: 'noContainer', + }); + return; + } + + const isRenderWrapper = innerNode.object.name === renderResultVarName; + containerCallsMethod = + ASTUtils.isIdentifier(innerNode.property) && + innerNode.property.name === 'container' && + isRenderWrapper; + + if (containerCallsMethod) { + context.report({ + node: innerNode.property, + messageId: 'noContainer', + }); + return; + } + } + showErrorIfChainedContainerMethod( + innerNode.object as TSESTree.MemberExpression + ); + } + } + + return { + CallExpression(node) { + const callExpressionIdentifier = getDeepestIdentifierNode(node); + + if (!callExpressionIdentifier) { + return; + } + + if (helpers.isRenderUtil(callExpressionIdentifier)) { + detectRenderWrapper(callExpressionIdentifier); + } + + if (isMemberExpression(node.callee)) { + showErrorIfChainedContainerMethod(node.callee); + } else if ( + ASTUtils.isIdentifier(node.callee) && + destructuredContainerPropNames.includes(node.callee.name) + ) { + context.report({ + node, + messageId: 'noContainer', + }); + } + }, + + VariableDeclarator(node) { + if (!node.init) { + return; + } + const initIdentifierNode = getDeepestIdentifierNode(node.init); + + if (!initIdentifierNode) { + return; + } + + const isRenderWrapperVariableDeclarator = renderWrapperNames.includes( + initIdentifierNode.name + ); + + if ( + !helpers.isRenderVariableDeclarator(node) && + !isRenderWrapperVariableDeclarator + ) { + return; + } + + if (isObjectPattern(node.id)) { + const containerIndex = node.id.properties.findIndex( + (property) => + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + property.key.name === 'container' + ); + + const nodeValue = + containerIndex !== -1 && node.id.properties[containerIndex].value; + + if (!nodeValue) { + return; + } + + if (ASTUtils.isIdentifier(nodeValue)) { + containerName = nodeValue.name; + } else if (isObjectPattern(nodeValue)) { + nodeValue.properties.forEach( + (property) => + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + destructuredContainerPropNames.push(property.key.name) + ); + } + } else if (ASTUtils.isIdentifier(node.id)) { + renderResultVarName = node.id.name; + } + }, + }; + }, }); diff --git a/lib/rules/no-debugging-utils.ts b/lib/rules/no-debugging-utils.ts index a6357cd5..470c2028 100644 --- a/lib/rules/no-debugging-utils.ts +++ b/lib/rules/no-debugging-utils.ts @@ -1,196 +1,200 @@ -import { - ASTUtils, - TSESTree, - JSONSchema, -} from '@typescript-eslint/experimental-utils'; +import { ASTUtils, TSESTree, JSONSchema } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - getDeepestIdentifierNode, - getFunctionName, - getInnermostReturningFunction, - getPropertyIdentifierNode, - getReferenceNode, - isCallExpression, - isObjectPattern, - isProperty, + getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, + getPropertyIdentifierNode, + getReferenceNode, + isCallExpression, + isObjectPattern, + isProperty, } from '../node-utils'; -import { DEBUG_UTILS } from '../utils'; +import { DEBUG_UTILS, getDeclaredVariables } from '../utils'; -type DebugUtilsToCheckFor = Partial< - Record ->; +type DebugUtilsToCheckForConfig = Record<(typeof DEBUG_UTILS)[number], boolean>; +type DebugUtilsToCheckFor = Partial; export const RULE_NAME = 'no-debugging-utils'; export type MessageIds = 'noDebug'; type Options = [{ utilsToCheckFor?: DebugUtilsToCheckFor }]; +const defaultUtilsToCheckFor: DebugUtilsToCheckForConfig = { + debug: true, + logTestingPlaygroundURL: true, + prettyDOM: true, + logRoles: true, + logDOM: true, + prettyFormat: true, +}; + export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: 'Disallow the use of debugging utilities like `debug`', - recommendedConfig: { - dom: false, - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - noDebug: 'Unexpected debug statement', - }, - schema: [ - { - type: 'object', - properties: { - utilsToCheckFor: { - type: 'object', - properties: DEBUG_UTILS.reduce< - Record - >( - (obj, name) => ({ - [name]: { type: 'boolean' }, - ...obj, - }), - {} - ), - additionalProperties: false, - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { utilsToCheckFor: { debug: true, logTestingPlaygroundURL: true } }, - ], - - create(context, [{ utilsToCheckFor = {} }], helpers) { - const suspiciousDebugVariableNames: string[] = []; - const suspiciousReferenceNodes: TSESTree.Identifier[] = []; - const renderWrapperNames: string[] = []; - const builtInConsoleNodes: TSESTree.VariableDeclarator[] = []; - - const utilsToReport = Object.entries(utilsToCheckFor) - .filter(([, shouldCheckFor]) => shouldCheckFor) - .map(([name]) => name); - - function detectRenderWrapper(node: TSESTree.Identifier): void { - const innerFunction = getInnermostReturningFunction(context, node); - - if (innerFunction) { - renderWrapperNames.push(getFunctionName(innerFunction)); - } - } - - return { - VariableDeclarator(node) { - if (!node.init) { - return; - } - const initIdentifierNode = getDeepestIdentifierNode(node.init); - - if (!initIdentifierNode) { - return; - } - - if (initIdentifierNode.name === 'console') { - builtInConsoleNodes.push(node); - return; - } - - const isRenderWrapperVariableDeclarator = renderWrapperNames.includes( - initIdentifierNode.name - ); - - if ( - !helpers.isRenderVariableDeclarator(node) && - !isRenderWrapperVariableDeclarator - ) { - return; - } - - // find debug obtained from render and save their name, like: - // const { debug } = render(); - if (isObjectPattern(node.id)) { - for (const property of node.id.properties) { - if ( - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - utilsToReport.includes(property.key.name) - ) { - const identifierNode = getDeepestIdentifierNode(property.value); - - if (identifierNode) { - suspiciousDebugVariableNames.push(identifierNode.name); - } - } - } - } - - // find utils kept from render and save their node, like: - // const utils = render(); - if (ASTUtils.isIdentifier(node.id)) { - suspiciousReferenceNodes.push(node.id); - } - }, - CallExpression(node) { - const callExpressionIdentifier = getDeepestIdentifierNode(node); - - if (!callExpressionIdentifier) { - return; - } - - if (helpers.isRenderUtil(callExpressionIdentifier)) { - detectRenderWrapper(callExpressionIdentifier); - } - - const referenceNode = getReferenceNode(node); - const referenceIdentifier = getPropertyIdentifierNode(referenceNode); - - if (!referenceIdentifier) { - return; - } - - const isDebugUtil = helpers.isDebugUtil( - callExpressionIdentifier, - utilsToReport as Array - ); - const isDeclaredDebugVariable = suspiciousDebugVariableNames.includes( - callExpressionIdentifier.name - ); - const isChainedReferenceDebug = suspiciousReferenceNodes.some( - (suspiciousReferenceIdentifier) => { - return ( - utilsToReport.includes(callExpressionIdentifier.name) && - suspiciousReferenceIdentifier.name === referenceIdentifier.name - ); - } - ); - - const isVariableFromBuiltInConsole = builtInConsoleNodes.some( - (variableDeclarator) => { - const variables = context.getDeclaredVariables(variableDeclarator); - return variables.some( - ({ name }) => - name === callExpressionIdentifier.name && - isCallExpression(callExpressionIdentifier.parent) - ); - } - ); - - if ( - !isVariableFromBuiltInConsole && - (isDebugUtil || isDeclaredDebugVariable || isChainedReferenceDebug) - ) { - context.report({ - node: callExpressionIdentifier, - messageId: 'noDebug', - }); - } - }, - }; - }, + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Disallow the use of debugging utilities like `debug`', + recommendedConfig: { + dom: false, + angular: 'warn', + react: 'warn', + vue: 'warn', + svelte: 'warn', + marko: 'warn', + }, + }, + messages: { + noDebug: 'Unexpected debug statement', + }, + schema: [ + { + type: 'object', + properties: { + utilsToCheckFor: { + type: 'object', + properties: DEBUG_UTILS.reduce< + Record + >( + (obj, name) => ({ + [name]: { type: 'boolean' }, + ...obj, + }), + {} + ), + additionalProperties: false, + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [{ utilsToCheckFor: defaultUtilsToCheckFor }], + + create(context, [{ utilsToCheckFor = {} }], helpers) { + const suspiciousDebugVariableNames: string[] = []; + const suspiciousReferenceNodes: TSESTree.Identifier[] = []; + const renderWrapperNames: string[] = []; + const builtInConsoleNodes: TSESTree.VariableDeclarator[] = []; + + const utilsToReport = Object.entries(utilsToCheckFor) + .filter(([, shouldCheckFor]) => shouldCheckFor) + .map(([name]) => name); + + function detectRenderWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + renderWrapperNames.push(getFunctionName(innerFunction)); + } + } + + return { + VariableDeclarator(node) { + if (!node.init) { + return; + } + const initIdentifierNode = getDeepestIdentifierNode(node.init); + + if (!initIdentifierNode) { + return; + } + + if (initIdentifierNode.name === 'console') { + builtInConsoleNodes.push(node); + return; + } + + const isRenderWrapperVariableDeclarator = renderWrapperNames.includes( + initIdentifierNode.name + ); + + if ( + !helpers.isRenderVariableDeclarator(node) && + !isRenderWrapperVariableDeclarator + ) { + return; + } + + // find debug obtained from render and save their name, like: + // const { debug } = render(); + if (isObjectPattern(node.id)) { + for (const property of node.id.properties) { + if ( + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + utilsToReport.includes(property.key.name) + ) { + const identifierNode = getDeepestIdentifierNode(property.value); + + if (identifierNode) { + suspiciousDebugVariableNames.push(identifierNode.name); + } + } + } + } + + // find utils kept from render and save their node, like: + // const utils = render(); + if (ASTUtils.isIdentifier(node.id)) { + suspiciousReferenceNodes.push(node.id); + } + }, + CallExpression(node) { + const callExpressionIdentifier = getDeepestIdentifierNode(node); + + if (!callExpressionIdentifier) { + return; + } + + if (helpers.isRenderUtil(callExpressionIdentifier)) { + detectRenderWrapper(callExpressionIdentifier); + } + + const referenceNode = getReferenceNode(node); + const referenceIdentifier = getPropertyIdentifierNode(referenceNode); + + if (!referenceIdentifier) { + return; + } + + const isDebugUtil = helpers.isDebugUtil( + callExpressionIdentifier, + utilsToReport as Array<(typeof DEBUG_UTILS)[number]> + ); + const isDeclaredDebugVariable = suspiciousDebugVariableNames.includes( + callExpressionIdentifier.name + ); + const isChainedReferenceDebug = suspiciousReferenceNodes.some( + (suspiciousReferenceIdentifier) => { + return ( + utilsToReport.includes(callExpressionIdentifier.name) && + suspiciousReferenceIdentifier.name === referenceIdentifier.name + ); + } + ); + + const isVariableFromBuiltInConsole = builtInConsoleNodes.some( + (variableDeclarator) => { + const variables = getDeclaredVariables(context, variableDeclarator); + return variables.some( + ({ name }) => + name === callExpressionIdentifier.name && + isCallExpression(callExpressionIdentifier.parent) + ); + } + ); + + if ( + !isVariableFromBuiltInConsole && + (isDebugUtil || isDeclaredDebugVariable || isChainedReferenceDebug) + ) { + context.report({ + node: callExpressionIdentifier, + messageId: 'noDebug', + }); + } + }, + }; + }, }); diff --git a/lib/rules/no-dom-import.ts b/lib/rules/no-dom-import.ts index 9ae585ee..974c34fa 100644 --- a/lib/rules/no-dom-import.ts +++ b/lib/rules/no-dom-import.ts @@ -1,103 +1,116 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { isCallExpression } from '../node-utils'; +import { isCallExpression, getImportModuleName } from '../node-utils'; export const RULE_NAME = 'no-dom-import'; export type MessageIds = 'noDomImport' | 'noDomImportFramework'; type Options = [string]; const DOM_TESTING_LIBRARY_MODULES = [ - 'dom-testing-library', - '@testing-library/dom', + 'dom-testing-library', + '@testing-library/dom', ]; +const CORRECT_MODULE_NAME_BY_FRAMEWORK: Record< + 'angular' | 'marko' | (string & NonNullable), + string | undefined +> = { + angular: '@testing-library/angular', // ATL is *always* called `@testing-library/angular` + marko: '@marko/testing-library', // Marko TL is called `@marko/testing-library` +}; +const getCorrectModuleName = ( + moduleName: string, + framework: string +): string => { + return ( + CORRECT_MODULE_NAME_BY_FRAMEWORK[framework] ?? + moduleName.replace('dom', framework) + ); +}; + export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: 'Disallow importing from DOM Testing Library', - recommendedConfig: { - dom: false, - angular: ['error', 'angular'], - react: ['error', 'react'], - vue: ['error', 'vue'], - }, - }, - messages: { - noDomImport: - 'import from DOM Testing Library is restricted, import from corresponding Testing Library framework instead', - noDomImportFramework: - 'import from DOM Testing Library is restricted, import from {{module}} instead', - }, - fixable: 'code', - schema: [ - { - type: 'string', - }, - ], - }, - defaultOptions: [''], + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Disallow importing from DOM Testing Library', + recommendedConfig: { + dom: false, + angular: ['error', 'angular'], + react: ['error', 'react'], + vue: ['error', 'vue'], + svelte: ['error', 'svelte'], + marko: ['error', 'marko'], + }, + }, + messages: { + noDomImport: + 'import from DOM Testing Library is restricted, import from corresponding Testing Library framework instead', + noDomImportFramework: + 'import from DOM Testing Library is restricted, import from {{module}} instead', + }, + fixable: 'code', + schema: [{ type: 'string' }], + }, + defaultOptions: [''], + + create(context, [framework], helpers) { + function report( + node: TSESTree.CallExpression | TSESTree.ImportDeclaration, + moduleName: string + ) { + if (!framework) { + return context.report({ + node, + messageId: 'noDomImport', + }); + } - create(context, [framework], helpers) { - function report( - node: TSESTree.CallExpression | TSESTree.ImportDeclaration, - moduleName: string - ) { - if (framework) { - const correctModuleName = moduleName.replace('dom', framework); - context.report({ - node, - messageId: 'noDomImportFramework', - data: { - module: correctModuleName, - }, - fix(fixer) { - if (isCallExpression(node)) { - const name = node.arguments[0] as TSESTree.Literal; + const correctModuleName = getCorrectModuleName(moduleName, framework); + context.report({ + data: { module: correctModuleName }, + fix(fixer) { + if (isCallExpression(node)) { + const name = node.arguments[0] as TSESTree.Literal; - // Replace the module name with the raw module name as we can't predict which punctuation the user is going to use - return fixer.replaceText( - name, - name.raw.replace(moduleName, correctModuleName) - ); - } else { - const name = node.source; - return fixer.replaceText( - name, - name.raw.replace(moduleName, correctModuleName) - ); - } - }, - }); - } else { - context.report({ - node, - messageId: 'noDomImport', - }); - } - } + // Replace the module name with the raw module name as we can't predict which punctuation the user is going to use + return fixer.replaceText( + name, + name.raw.replace(moduleName, correctModuleName) + ); + } else { + const name = node.source; + return fixer.replaceText( + name, + name.raw.replace(moduleName, correctModuleName) + ); + } + }, + messageId: 'noDomImportFramework', + node, + }); + } - return { - 'Program:exit'() { - const importName = helpers.getTestingLibraryImportName(); - const importNode = helpers.getTestingLibraryImportNode(); + return { + 'Program:exit'() { + let importName: string | undefined; + const allImportNodes = helpers.getAllTestingLibraryImportNodes(); - if (!importNode) { - return; - } + allImportNodes.forEach((importNode) => { + importName = getImportModuleName(importNode); - const domModuleName = DOM_TESTING_LIBRARY_MODULES.find( - (module) => module === importName - ); + const domModuleName = DOM_TESTING_LIBRARY_MODULES.find( + (module) => module === importName + ); - if (!domModuleName) { - return; - } + if (!domModuleName) { + return; + } - report(importNode, domModuleName); - }, - }; - }, + report(importNode, domModuleName); + }); + }, + }; + }, }); diff --git a/lib/rules/no-global-regexp-flag-in-query.ts b/lib/rules/no-global-regexp-flag-in-query.ts new file mode 100644 index 00000000..50293045 --- /dev/null +++ b/lib/rules/no-global-regexp-flag-in-query.ts @@ -0,0 +1,179 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; + +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { + isMemberExpression, + isCallExpression, + isProperty, + isObjectExpression, + getDeepestIdentifierNode, + isLiteral, +} from '../node-utils'; + +export const RULE_NAME = 'no-global-regexp-flag-in-query'; +export type MessageIds = 'noGlobalRegExpFlagInQuery'; +type Options = []; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: 'Disallow the use of the global RegExp flag (/g) in queries', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + noGlobalRegExpFlagInQuery: + 'Avoid using the global RegExp flag (/g) in queries', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context, _, helpers) { + /** + * Checks if node is reportable (has a regex that contains 'g') and if it is, reports it with `context.report()`. + * + * @param literalNode Literal node under to be + * @returns {Boolean} indicatinf if literal was reported + */ + function reportLiteralWithRegex(literalNode: TSESTree.Node) { + if ( + isLiteral(literalNode) && + 'regex' in literalNode && + literalNode.regex.flags.includes('g') + ) { + context.report({ + node: literalNode, + messageId: 'noGlobalRegExpFlagInQuery', + fix(fixer) { + const splitter = literalNode.raw.lastIndexOf('/'); + const raw = literalNode.raw.substring(0, splitter); + const flags = literalNode.raw.substring(splitter + 1); + const flagsWithoutGlobal = flags.replace('g', ''); + + return fixer.replaceText( + literalNode, + `${raw}/${flagsWithoutGlobal}` + ); + }, + }); + return true; + } + return false; + } + + function getArguments(identifierNode: TSESTree.Identifier) { + if (isCallExpression(identifierNode.parent)) { + return identifierNode.parent.arguments; + } else if ( + isMemberExpression(identifierNode.parent) && + isCallExpression(identifierNode.parent.parent) + ) { + return identifierNode.parent.parent.arguments; + } + + return []; + } + + // Helper array to store variable nodes that have a literal with regex + // e.g. `const countRegExp = /count/gi` will be store here + const variableNodesWithRegexs: TSESTree.VariableDeclarator[] = []; + + function hasRegexInVariable( + identifier: TSESTree.Identifier + ): TSESTree.VariableDeclarator | undefined { + return variableNodesWithRegexs.find((varNode) => { + if ( + ASTUtils.isVariableDeclarator(varNode) && + ASTUtils.isIdentifier(varNode.id) + ) { + return varNode.id.name === identifier.name; + } + return undefined; + }); + } + + return { + // internal helper function, helps store all variables with regex to `variableNodesWithRegexs` + // could potentially be refactored to using context.getDeclaredVariables() + VariableDeclarator(node: TSESTree.Node) { + if ( + ASTUtils.isVariableDeclarator(node) && + isLiteral(node.init) && + 'regex' in node.init && + node.init.regex.flags.includes('g') + ) { + variableNodesWithRegexs.push(node); + } + }, + CallExpression(node) { + const identifierNode = getDeepestIdentifierNode(node); + if (!identifierNode || !helpers.isQuery(identifierNode)) { + return; + } + + const [firstArg, secondArg] = getArguments(identifierNode); + + const firstArgumentHasError = reportLiteralWithRegex(firstArg); + if (firstArgumentHasError) { + return; + } + + // Case issue #592: a variable that has a regex is passed to testing library query + + if (ASTUtils.isIdentifier(firstArg)) { + const regexVariableNode = hasRegexInVariable(firstArg); + if (regexVariableNode !== undefined) { + context.report({ + node: firstArg, + messageId: 'noGlobalRegExpFlagInQuery', + fix(fixer) { + if ( + ASTUtils.isVariableDeclarator(regexVariableNode) && + isLiteral(regexVariableNode.init) && + 'regex' in regexVariableNode.init && + regexVariableNode.init.regex.flags.includes('g') + ) { + const splitter = regexVariableNode.init.raw.lastIndexOf('/'); + const raw = regexVariableNode.init.raw.substring(0, splitter); + const flags = regexVariableNode.init.raw.substring( + splitter + 1 + ); + const flagsWithoutGlobal = flags.replace('g', ''); + + return fixer.replaceText( + regexVariableNode.init, + `${raw}/${flagsWithoutGlobal}` + ); + } + return null; + }, + }); + } + } + + if (isObjectExpression(secondArg)) { + const namePropertyNode = secondArg.properties.find( + (p) => + isProperty(p) && + ASTUtils.isIdentifier(p.key) && + p.key.name === 'name' && + isLiteral(p.value) + ) as TSESTree.Property | undefined; + + if (namePropertyNode) { + reportLiteralWithRegex(namePropertyNode.value); + } + } + }, + }; + }, +}); diff --git a/lib/rules/no-manual-cleanup.ts b/lib/rules/no-manual-cleanup.ts index 45d4a1c2..5e22d307 100644 --- a/lib/rules/no-manual-cleanup.ts +++ b/lib/rules/no-manual-cleanup.ts @@ -1,134 +1,135 @@ -import { - ASTUtils, - TSESTree, - TSESLint, -} from '@typescript-eslint/experimental-utils'; +import { ASTUtils, TSESLint, TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - getVariableReferences, - isImportDefaultSpecifier, - isImportSpecifier, - isMemberExpression, - isObjectPattern, - isProperty, - ImportModuleNode, - isImportDeclaration, + getImportModuleName, + getVariableReferences, + ImportModuleNode, + isImportDeclaration, + isImportDefaultSpecifier, + isImportSpecifier, + isMemberExpression, + isObjectPattern, + isProperty, } from '../node-utils'; +import { getDeclaredVariables } from '../utils'; export const RULE_NAME = 'no-manual-cleanup'; export type MessageIds = 'noManualCleanup'; type Options = []; const CLEANUP_LIBRARY_REGEXP = - /(@testing-library\/(preact|react|svelte|vue))|@marko\/testing-library/; + /(@testing-library\/(preact|react|svelte|vue))|@marko\/testing-library/; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: 'Disallow the use of `cleanup`', - recommendedConfig: { - dom: false, - angular: false, - react: false, - vue: false, - }, - }, - messages: { - noManualCleanup: - "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", - }, - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - function reportImportReferences(references: TSESLint.Scope.Reference[]) { - for (const reference of references) { - const utilsUsage = reference.identifier.parent; - - if ( - utilsUsage && - isMemberExpression(utilsUsage) && - ASTUtils.isIdentifier(utilsUsage.property) && - utilsUsage.property.name === 'cleanup' - ) { - context.report({ - node: utilsUsage.property, - messageId: 'noManualCleanup', - }); - } - } - } - - function reportCandidateModule(moduleNode: ImportModuleNode) { - if (isImportDeclaration(moduleNode)) { - // case: import utils from 'testing-library-module' - if (isImportDefaultSpecifier(moduleNode.specifiers[0])) { - const { references } = context.getDeclaredVariables(moduleNode)[0]; - - reportImportReferences(references); - } - - // case: import { cleanup } from 'testing-library-module' - const cleanupSpecifier = moduleNode.specifiers.find( - (specifier) => - isImportSpecifier(specifier) && - specifier.imported.name === 'cleanup' - ); - - if (cleanupSpecifier) { - context.report({ - node: cleanupSpecifier, - messageId: 'noManualCleanup', - }); - } - } else { - const declaratorNode = moduleNode.parent as TSESTree.VariableDeclarator; - - if (isObjectPattern(declaratorNode.id)) { - // case: const { cleanup } = require('testing-library-module') - const cleanupProperty = declaratorNode.id.properties.find( - (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - property.key.name === 'cleanup' - ); - - if (cleanupProperty) { - context.report({ - node: cleanupProperty, - messageId: 'noManualCleanup', - }); - } - } else { - // case: const utils = require('testing-library-module') - const references = getVariableReferences(context, declaratorNode); - reportImportReferences(references); - } - } - } - - return { - 'Program:exit'() { - const testingLibraryImportName = helpers.getTestingLibraryImportName(); - const testingLibraryImportNode = helpers.getTestingLibraryImportNode(); - const customModuleImportNode = helpers.getCustomModuleImportNode(); - - if ( - testingLibraryImportName && - testingLibraryImportNode && - testingLibraryImportName.match(CLEANUP_LIBRARY_REGEXP) - ) { - reportCandidateModule(testingLibraryImportNode); - } - - if (customModuleImportNode) { - reportCandidateModule(customModuleImportNode); - } - }, - }; - }, + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Disallow the use of `cleanup`', + recommendedConfig: { + dom: false, + angular: false, + react: 'error', + vue: 'error', + svelte: 'error', + marko: false, + }, + }, + messages: { + noManualCleanup: + "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", + }, + schema: [], + }, + defaultOptions: [], + + create(context, _, helpers) { + function reportImportReferences(references: TSESLint.Scope.Reference[]) { + for (const reference of references) { + const utilsUsage = reference.identifier.parent; + + if ( + utilsUsage && + isMemberExpression(utilsUsage) && + ASTUtils.isIdentifier(utilsUsage.property) && + utilsUsage.property.name === 'cleanup' + ) { + context.report({ + node: utilsUsage.property, + messageId: 'noManualCleanup', + }); + } + } + } + + function reportCandidateModule(moduleNode: ImportModuleNode) { + if (isImportDeclaration(moduleNode)) { + // case: import utils from 'testing-library-module' + if (isImportDefaultSpecifier(moduleNode.specifiers[0])) { + const { references } = getDeclaredVariables(context, moduleNode)[0]; + + reportImportReferences(references); + } + + // case: import { cleanup } from 'testing-library-module' + const cleanupSpecifier = moduleNode.specifiers.find( + (specifier) => + isImportSpecifier(specifier) && + ASTUtils.isIdentifier(specifier.imported) && + specifier.imported.name === 'cleanup' + ); + + if (cleanupSpecifier) { + context.report({ + node: cleanupSpecifier, + messageId: 'noManualCleanup', + }); + } + } else { + const declaratorNode = moduleNode.parent as TSESTree.VariableDeclarator; + + if (isObjectPattern(declaratorNode.id)) { + // case: const { cleanup } = require('testing-library-module') + const cleanupProperty = declaratorNode.id.properties.find( + (property) => + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + property.key.name === 'cleanup' + ); + + if (cleanupProperty) { + context.report({ + node: cleanupProperty, + messageId: 'noManualCleanup', + }); + } + } else { + // case: const utils = require('testing-library-module') + const references = getVariableReferences(context, declaratorNode); + reportImportReferences(references); + } + } + } + + return { + 'Program:exit'() { + const customModuleImportNode = helpers.getCustomModuleImportNode(); + + for (const testingLibraryImportNode of helpers.getAllTestingLibraryImportNodes()) { + const testingLibraryImportName = getImportModuleName( + testingLibraryImportNode + ); + + if (testingLibraryImportName?.match(CLEANUP_LIBRARY_REGEXP)) { + reportCandidateModule(testingLibraryImportNode); + } + } + + if (customModuleImportNode) { + reportCandidateModule(customModuleImportNode); + } + }, + }; + }, }); diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index 36d232be..9bc3a713 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -1,57 +1,196 @@ -import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; +import { + DefinitionType, + type ScopeVariable, +} from '@typescript-eslint/scope-manager'; +import { TSESTree, ASTUtils } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { ALL_RETURNING_NODES } from '../utils'; +import { + getDeepestIdentifierNode, + getPropertyIdentifierNode, + isCallExpression, + isMemberExpression, +} from '../node-utils'; +import { + ALL_RETURNING_NODES, + EVENT_HANDLER_METHODS, + getScope, + resolveToTestingLibraryFn, +} from '../utils'; export const RULE_NAME = 'no-node-access'; export type MessageIds = 'noNodeAccess'; -type Options = []; +export type Options = [{ allowContainerFirstChild: boolean }]; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: 'Disallow direct Node access', - recommendedConfig: { - dom: false, - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - noNodeAccess: - 'Avoid direct Node access. Prefer using the methods from Testing Library.', - }, - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - function showErrorForNodeAccess(node: TSESTree.MemberExpression) { - // This rule is so aggressive that can cause tons of false positives outside test files when Aggressive Reporting - // is enabled. Because of that, this rule will skip this mechanism and report only if some Testing Library package - // or custom one (set in utils-module Shared Setting) is found. - if (!helpers.isTestingLibraryImported(true)) { - return; - } - - if ( - ASTUtils.isIdentifier(node.property) && - ALL_RETURNING_NODES.includes(node.property.name) - ) { - context.report({ - node, - loc: node.property.loc.start, - messageId: 'noNodeAccess', - }); - } - } - - return { - 'ExpressionStatement MemberExpression': showErrorForNodeAccess, - 'VariableDeclarator MemberExpression': showErrorForNodeAccess, - }; - }, + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Disallow direct Node access', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + noNodeAccess: + 'Avoid direct Node access. Prefer using the methods from Testing Library.', + }, + schema: [ + { + type: 'object', + properties: { + allowContainerFirstChild: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [ + { + allowContainerFirstChild: false, + }, + ], + + create(context, [{ allowContainerFirstChild = false }], helpers) { + const userEventInstanceNames = new Set(); + + function showErrorForNodeAccess(node: TSESTree.MemberExpression) { + // This rule is so aggressive that can cause tons of false positives outside test files when Aggressive Reporting + // is enabled. Because of that, this rule will skip this mechanism and report only if some Testing Library package + // or custom one (set in utils-module Shared Setting) is found. + if (!helpers.isTestingLibraryImported(true)) { + return; + } + + const propertyName = ASTUtils.isIdentifier(node.property) + ? node.property.name + : null; + + if ( + propertyName && + ALL_RETURNING_NODES.some( + (allReturningNode) => allReturningNode === propertyName + ) + ) { + if (allowContainerFirstChild && propertyName === 'firstChild') { + return; + } + + if ( + ASTUtils.isIdentifier(node.object) && + node.object.name === 'props' + ) { + return; + } + + context.report({ + node, + loc: node.property.loc.start, + messageId: 'noNodeAccess', + }); + } + } + + function detectTestingLibraryFn( + node: TSESTree.CallExpression, + variable: ScopeVariable | null + ) { + if (variable && variable.defs.length > 0) { + const def = variable.defs[0]; + if ( + def.type === DefinitionType.Variable && + isCallExpression(def.node.init) + ) { + return resolveToTestingLibraryFn(def.node.init, context); + } + } + + return resolveToTestingLibraryFn(node, context); + } + + return { + CallExpression(node: TSESTree.CallExpression) { + const property = getDeepestIdentifierNode(node); + const identifier = getPropertyIdentifierNode(node); + + const isEventHandlerMethod = EVENT_HANDLER_METHODS.some( + (method) => method === property?.name + ); + const hasUserEventInstanceName = userEventInstanceNames.has( + identifier?.name ?? '' + ); + + const variable = identifier + ? ASTUtils.findVariable(getScope(context, node), identifier) + : null; + const testingLibraryFn = detectTestingLibraryFn(node, variable); + + if ( + !testingLibraryFn && + isEventHandlerMethod && + !hasUserEventInstanceName + ) { + context.report({ + node, + loc: property?.loc.start, + messageId: 'noNodeAccess', + }); + } + }, + VariableDeclarator(node: TSESTree.VariableDeclarator) { + const { init, id } = node; + + if (!isCallExpression(init)) { + return; + } + + if ( + !isMemberExpression(init.callee) || + !ASTUtils.isIdentifier(init.callee.object) + ) { + return; + } + + const testingLibraryFn = resolveToTestingLibraryFn(init, context); + if ( + init.callee.object.name === testingLibraryFn?.local && + ASTUtils.isIdentifier(init.callee.property) && + init.callee.property.name === 'setup' && + ASTUtils.isIdentifier(id) + ) { + userEventInstanceNames.add(id.name); + } + }, + AssignmentExpression(node: TSESTree.AssignmentExpression) { + if ( + ASTUtils.isIdentifier(node.left) && + isCallExpression(node.right) && + isMemberExpression(node.right.callee) && + ASTUtils.isIdentifier(node.right.callee.object) + ) { + const testingLibraryFn = resolveToTestingLibraryFn( + node.right, + context + ); + if ( + node.right.callee.object.name === testingLibraryFn?.local && + ASTUtils.isIdentifier(node.right.callee.property) && + node.right.callee.property.name === 'setup' + ) { + userEventInstanceNames.add(node.left.name); + } + } + }, + 'ExpressionStatement MemberExpression': showErrorForNodeAccess, + 'VariableDeclarator MemberExpression': showErrorForNodeAccess, + }; + }, }); diff --git a/lib/rules/no-promise-in-fire-event.ts b/lib/rules/no-promise-in-fire-event.ts index 01027201..a60967ec 100644 --- a/lib/rules/no-promise-in-fire-event.ts +++ b/lib/rules/no-promise-in-fire-event.ts @@ -1,113 +1,116 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - findClosestCallExpressionNode, - getDeepestIdentifierNode, - isCallExpression, - isNewExpression, - isPromiseIdentifier, + findClosestCallExpressionNode, + getDeepestIdentifierNode, + isCallExpression, + isNewExpression, + isPromiseIdentifier, } from '../node-utils'; +import { getScope } from '../utils'; export const RULE_NAME = 'no-promise-in-fire-event'; export type MessageIds = 'noPromiseInFireEvent'; type Options = []; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: - 'Disallow the use of promises passed to a `fireEvent` method', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - noPromiseInFireEvent: - "A promise shouldn't be passed to a `fireEvent` method, instead pass the DOM element", - }, - schema: [], - }, - defaultOptions: [], + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: + 'Disallow the use of promises passed to a `fireEvent` method', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + noPromiseInFireEvent: + "A promise shouldn't be passed to a `fireEvent` method, instead pass the DOM element", + }, + schema: [], + }, + defaultOptions: [], - create(context, _, helpers) { - function checkSuspiciousNode( - node: TSESTree.Node, - originalNode?: TSESTree.Node - ): void { - if (ASTUtils.isAwaitExpression(node)) { - return; - } + create(context, _, helpers) { + function checkSuspiciousNode( + node: TSESTree.Node, + originalNode?: TSESTree.Node + ): void { + if (ASTUtils.isAwaitExpression(node)) { + return; + } - if (isNewExpression(node)) { - if (isPromiseIdentifier(node.callee)) { - context.report({ - node: originalNode ?? node, - messageId: 'noPromiseInFireEvent', - }); - return; - } - } + if (isNewExpression(node)) { + if (isPromiseIdentifier(node.callee)) { + context.report({ + node: originalNode ?? node, + messageId: 'noPromiseInFireEvent', + }); + return; + } + } - if (isCallExpression(node)) { - const domElementIdentifier = getDeepestIdentifierNode(node); + if (isCallExpression(node)) { + const domElementIdentifier = getDeepestIdentifierNode(node); - if (!domElementIdentifier) { - return; - } + if (!domElementIdentifier) { + return; + } - if ( - helpers.isAsyncQuery(domElementIdentifier) || - isPromiseIdentifier(domElementIdentifier) - ) { - context.report({ - node: originalNode ?? node, - messageId: 'noPromiseInFireEvent', - }); - return; - } - } + if ( + helpers.isAsyncQuery(domElementIdentifier) || + isPromiseIdentifier(domElementIdentifier) + ) { + context.report({ + node: originalNode ?? node, + messageId: 'noPromiseInFireEvent', + }); + return; + } + } - if (ASTUtils.isIdentifier(node)) { - const nodeVariable = ASTUtils.findVariable( - context.getScope(), - node.name - ); - if (!nodeVariable) { - return; - } + if (ASTUtils.isIdentifier(node)) { + const nodeVariable = ASTUtils.findVariable( + getScope(context, node), + node.name + ); + if (!nodeVariable) { + return; + } - for (const definition of nodeVariable.defs) { - const variableDeclarator = - definition.node as TSESTree.VariableDeclarator; - if (variableDeclarator.init) { - checkSuspiciousNode(variableDeclarator.init, node); - } - } - } - } + for (const definition of nodeVariable.defs) { + const variableDeclarator = + definition.node as TSESTree.VariableDeclarator; + if (variableDeclarator.init) { + checkSuspiciousNode(variableDeclarator.init, node); + } + } + } + } - return { - 'CallExpression Identifier'(node: TSESTree.Identifier) { - if (!helpers.isFireEventMethod(node)) { - return; - } + return { + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if (!helpers.isFireEventMethod(node)) { + return; + } - const closestCallExpression = findClosestCallExpressionNode(node, true); + const closestCallExpression = findClosestCallExpressionNode(node, true); - if (!closestCallExpression) { - return; - } + if (!closestCallExpression) { + return; + } - const domElementArgument = closestCallExpression.arguments[0]; + const domElementArgument = closestCallExpression.arguments[0]; - checkSuspiciousNode(domElementArgument); - }, - }; - }, + checkSuspiciousNode(domElementArgument); + }, + }; + }, }); diff --git a/lib/rules/no-render-in-lifecycle.ts b/lib/rules/no-render-in-lifecycle.ts new file mode 100644 index 00000000..cb761378 --- /dev/null +++ b/lib/rules/no-render-in-lifecycle.ts @@ -0,0 +1,142 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; + +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { + getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, + isCallExpression, +} from '../node-utils'; +import { TESTING_FRAMEWORK_SETUP_HOOKS } from '../utils'; + +export const RULE_NAME = 'no-render-in-lifecycle'; +export type MessageIds = 'noRenderInSetup'; +type Options = [ + { + allowTestingFrameworkSetupHook?: string; + }, +]; + +export function findClosestBeforeHook( + node: TSESTree.Node | null, + testingFrameworkSetupHooksToFilter: string[] +): TSESTree.Identifier | null { + if (node === null) { + return null; + } + + if ( + isCallExpression(node) && + ASTUtils.isIdentifier(node.callee) && + testingFrameworkSetupHooksToFilter.includes(node.callee.name) + ) { + return node.callee; + } + + if (node.parent) { + return findClosestBeforeHook( + node.parent, + testingFrameworkSetupHooksToFilter + ); + } + + return null; +} + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: + 'Disallow the use of `render` in testing frameworks setup functions', + recommendedConfig: { + dom: false, + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + noRenderInSetup: + 'Forbidden usage of `render` within testing framework `{{ name }}` setup', + }, + schema: [ + { + type: 'object', + properties: { + allowTestingFrameworkSetupHook: { + enum: [...TESTING_FRAMEWORK_SETUP_HOOKS], + type: 'string', + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [ + { + allowTestingFrameworkSetupHook: '', + }, + ], + + create(context, [{ allowTestingFrameworkSetupHook }], helpers) { + const renderWrapperNames: string[] = []; + + function detectRenderWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + renderWrapperNames.push(getFunctionName(innerFunction)); + } + } + + return { + CallExpression(node) { + const testingFrameworkSetupHooksToFilter = + TESTING_FRAMEWORK_SETUP_HOOKS.filter( + (hook) => hook !== allowTestingFrameworkSetupHook + ); + const callExpressionIdentifier = getDeepestIdentifierNode(node); + + if (!callExpressionIdentifier) { + return; + } + + const isRenderIdentifier = helpers.isRenderUtil( + callExpressionIdentifier + ); + + if (isRenderIdentifier) { + detectRenderWrapper(callExpressionIdentifier); + } + + if ( + !isRenderIdentifier && + !renderWrapperNames.includes(callExpressionIdentifier.name) + ) { + return; + } + + const beforeHook = findClosestBeforeHook( + node, + testingFrameworkSetupHooksToFilter + ); + + if (!beforeHook) { + return; + } + + context.report({ + node: callExpressionIdentifier, + messageId: 'noRenderInSetup', + data: { + name: beforeHook.name, + }, + }); + }, + }; + }, +}); diff --git a/lib/rules/no-render-in-setup.ts b/lib/rules/no-render-in-setup.ts deleted file mode 100644 index 29f9a442..00000000 --- a/lib/rules/no-render-in-setup.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; - -import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { - getDeepestIdentifierNode, - getFunctionName, - getInnermostReturningFunction, - isCallExpression, -} from '../node-utils'; -import { TESTING_FRAMEWORK_SETUP_HOOKS } from '../utils'; - -export const RULE_NAME = 'no-render-in-setup'; -export type MessageIds = 'noRenderInSetup'; -type Options = [ - { - allowTestingFrameworkSetupHook?: string; - } -]; - -export function findClosestBeforeHook( - node: TSESTree.Node | null, - testingFrameworkSetupHooksToFilter: string[] -): TSESTree.Identifier | null { - if (node === null) { - return null; - } - - if ( - isCallExpression(node) && - ASTUtils.isIdentifier(node.callee) && - testingFrameworkSetupHooksToFilter.includes(node.callee.name) - ) { - return node.callee; - } - - if (node.parent) { - return findClosestBeforeHook( - node.parent, - testingFrameworkSetupHooksToFilter - ); - } - - return null; -} - -export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: - 'Disallow the use of `render` in testing frameworks setup functions', - recommendedConfig: { - dom: false, - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - noRenderInSetup: - 'Forbidden usage of `render` within testing framework `{{ name }}` setup', - }, - schema: [ - { - type: 'object', - properties: { - allowTestingFrameworkSetupHook: { - enum: TESTING_FRAMEWORK_SETUP_HOOKS, - }, - }, - }, - ], - }, - defaultOptions: [ - { - allowTestingFrameworkSetupHook: '', - }, - ], - - create(context, [{ allowTestingFrameworkSetupHook }], helpers) { - const renderWrapperNames: string[] = []; - - function detectRenderWrapper(node: TSESTree.Identifier): void { - const innerFunction = getInnermostReturningFunction(context, node); - - if (innerFunction) { - renderWrapperNames.push(getFunctionName(innerFunction)); - } - } - - return { - CallExpression(node) { - const testingFrameworkSetupHooksToFilter = - TESTING_FRAMEWORK_SETUP_HOOKS.filter( - (hook) => hook !== allowTestingFrameworkSetupHook - ); - const callExpressionIdentifier = getDeepestIdentifierNode(node); - - if (!callExpressionIdentifier) { - return; - } - - const isRenderIdentifier = helpers.isRenderUtil( - callExpressionIdentifier - ); - - if (isRenderIdentifier) { - detectRenderWrapper(callExpressionIdentifier); - } - - if ( - !isRenderIdentifier && - !renderWrapperNames.includes(callExpressionIdentifier.name) - ) { - return; - } - - const beforeHook = findClosestBeforeHook( - node, - testingFrameworkSetupHooksToFilter - ); - - if (!beforeHook) { - return; - } - - context.report({ - node: callExpressionIdentifier, - messageId: 'noRenderInSetup', - data: { - name: beforeHook.name, - }, - }); - }, - }; - }, -}); diff --git a/lib/rules/no-test-id-queries.ts b/lib/rules/no-test-id-queries.ts new file mode 100644 index 00000000..7420f8d8 --- /dev/null +++ b/lib/rules/no-test-id-queries.ts @@ -0,0 +1,47 @@ +import { TSESTree } from '@typescript-eslint/utils'; + +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { ALL_QUERIES_VARIANTS } from '../utils'; + +export const RULE_NAME = 'no-test-id-queries'; +export type MessageIds = 'noTestIdQueries'; +type Options = []; + +const QUERIES_REGEX = `/^(${ALL_QUERIES_VARIANTS.join('|')})TestId$/`; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Ensure no `data-testid` queries are used', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + svelte: false, + marko: false, + }, + }, + messages: { + noTestIdQueries: + 'Using `data-testid` queries is not recommended. Use a more descriptive query instead.', + }, + schema: [], + }, + defaultOptions: [], + + create(context) { + return { + [`CallExpression[callee.property.name=${QUERIES_REGEX}], CallExpression[callee.name=${QUERIES_REGEX}]`]( + node: TSESTree.CallExpression + ) { + context.report({ + node, + messageId: 'noTestIdQueries', + }); + }, + }; + }, +}); diff --git a/lib/rules/no-unnecessary-act.ts b/lib/rules/no-unnecessary-act.ts index 5cd9f4b1..75fc9444 100644 --- a/lib/rules/no-unnecessary-act.ts +++ b/lib/rules/no-unnecessary-act.ts @@ -1,203 +1,206 @@ -import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; +import { TSESTree, ASTUtils } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - getDeepestIdentifierNode, - getPropertyIdentifierNode, - getStatementCallExpression, - isEmptyFunction, - isExpressionStatement, - isReturnStatement, + getDeepestIdentifierNode, + getPropertyIdentifierNode, + getStatementCallExpression, + isEmptyFunction, + isExpressionStatement, + isReturnStatement, } from '../node-utils'; export const RULE_NAME = 'no-unnecessary-act'; export type MessageIds = - | 'noUnnecessaryActEmptyFunction' - | 'noUnnecessaryActTestingLibraryUtil'; + | 'noUnnecessaryActEmptyFunction' + | 'noUnnecessaryActTestingLibraryUtil'; export type Options = [{ isStrict: boolean }]; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: - 'Disallow wrapping Testing Library utils or empty callbacks in `act`', - recommendedConfig: { - dom: false, - angular: false, - react: 'error', - vue: false, - }, - }, - messages: { - noUnnecessaryActTestingLibraryUtil: - 'Avoid wrapping Testing Library util calls in `act`', - noUnnecessaryActEmptyFunction: 'Avoid wrapping empty function in `act`', - }, - schema: [ - { - type: 'object', - properties: { - isStrict: { - type: 'boolean', - }, - }, - }, - ], - }, - defaultOptions: [ - { - isStrict: true, - }, - ], - - create(context, [{ isStrict = true }], helpers) { - function getStatementIdentifier(statement: TSESTree.Statement) { - const callExpression = getStatementCallExpression(statement); - - if ( - !callExpression && - !isExpressionStatement(statement) && - !isReturnStatement(statement) - ) { - return null; - } - - if (callExpression) { - return getDeepestIdentifierNode(callExpression); - } - - if ( - isExpressionStatement(statement) && - ASTUtils.isAwaitExpression(statement.expression) - ) { - return getPropertyIdentifierNode(statement.expression.argument); - } - - if (isReturnStatement(statement) && statement.argument) { - return getPropertyIdentifierNode(statement.argument); - } - - return null; - } - - /** - * Determines whether some call is non Testing Library related for a given list of statements. - */ - function hasSomeNonTestingLibraryCall( - statements: TSESTree.Statement[] - ): boolean { - return statements.some((statement) => { - const identifier = getStatementIdentifier(statement); - - if (!identifier) { - return false; - } - - return !helpers.isTestingLibraryUtil(identifier); - }); - } - - function hasTestingLibraryCall(statements: TSESTree.Statement[]) { - return statements.some((statement) => { - const identifier = getStatementIdentifier(statement); - - if (!identifier) { - return false; - } - - return helpers.isTestingLibraryUtil(identifier); - }); - } - - function checkNoUnnecessaryActFromBlockStatement( - blockStatementNode: TSESTree.BlockStatement - ) { - const functionNode = blockStatementNode.parent as - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionExpression - | undefined; - const callExpressionNode = functionNode?.parent as - | TSESTree.CallExpression - | undefined; - - if (!callExpressionNode || !functionNode) { - return; - } - - const identifierNode = getDeepestIdentifierNode(callExpressionNode); - if (!identifierNode) { - return; - } - - if (!helpers.isActUtil(identifierNode)) { - return; - } - - if (isEmptyFunction(functionNode)) { - context.report({ - node: identifierNode, - messageId: 'noUnnecessaryActEmptyFunction', - }); - return; - } - - const shouldBeReported = isStrict - ? hasTestingLibraryCall(blockStatementNode.body) - : !hasSomeNonTestingLibraryCall(blockStatementNode.body); - - if (shouldBeReported) { - context.report({ - node: identifierNode, - messageId: 'noUnnecessaryActTestingLibraryUtil', - }); - } - } - - function checkNoUnnecessaryActFromImplicitReturn( - node: TSESTree.CallExpression - ) { - const nodeIdentifier = getDeepestIdentifierNode(node); - - if (!nodeIdentifier) { - return; - } - - const parentCallExpression = node.parent?.parent as - | TSESTree.CallExpression - | undefined; - - if (!parentCallExpression) { - return; - } - - const identifierNode = getDeepestIdentifierNode(parentCallExpression); - if (!identifierNode) { - return; - } - - if (!helpers.isActUtil(identifierNode)) { - return; - } - - if (!helpers.isTestingLibraryUtil(nodeIdentifier)) { - return; - } - - context.report({ - node: identifierNode, - messageId: 'noUnnecessaryActTestingLibraryUtil', - }); - } - - return { - 'CallExpression > ArrowFunctionExpression > BlockStatement': - checkNoUnnecessaryActFromBlockStatement, - 'CallExpression > FunctionExpression > BlockStatement': - checkNoUnnecessaryActFromBlockStatement, - 'CallExpression > ArrowFunctionExpression > CallExpression': - checkNoUnnecessaryActFromImplicitReturn, - }; - }, + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: + 'Disallow wrapping Testing Library utils or empty callbacks in `act`', + recommendedConfig: { + dom: false, + angular: false, + react: 'error', + vue: false, + svelte: false, + marko: 'error', + }, + }, + messages: { + noUnnecessaryActTestingLibraryUtil: + 'Avoid wrapping Testing Library util calls in `act`', + noUnnecessaryActEmptyFunction: 'Avoid wrapping empty function in `act`', + }, + schema: [ + { + type: 'object', + properties: { + isStrict: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [ + { + isStrict: true, + }, + ], + + create(context, [{ isStrict = true }], helpers) { + function getStatementIdentifier(statement: TSESTree.Statement) { + const callExpression = getStatementCallExpression(statement); + + if ( + !callExpression && + !isExpressionStatement(statement) && + !isReturnStatement(statement) + ) { + return null; + } + + if (callExpression) { + return getDeepestIdentifierNode(callExpression); + } + + if ( + isExpressionStatement(statement) && + ASTUtils.isAwaitExpression(statement.expression) + ) { + return getPropertyIdentifierNode(statement.expression.argument); + } + + if (isReturnStatement(statement) && statement.argument) { + return getPropertyIdentifierNode(statement.argument); + } + + return null; + } + + /** + * Determines whether some call is non Testing Library related for a given list of statements. + */ + function hasSomeNonTestingLibraryCall( + statements: TSESTree.Statement[] + ): boolean { + return statements.some((statement) => { + const identifier = getStatementIdentifier(statement); + + if (!identifier) { + return false; + } + + return !helpers.isTestingLibraryUtil(identifier); + }); + } + + function hasTestingLibraryCall(statements: TSESTree.Statement[]) { + return statements.some((statement) => { + const identifier = getStatementIdentifier(statement); + + if (!identifier) { + return false; + } + + return helpers.isTestingLibraryUtil(identifier); + }); + } + + function checkNoUnnecessaryActFromBlockStatement( + blockStatementNode: TSESTree.BlockStatement + ) { + const functionNode = blockStatementNode.parent as + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression + | undefined; + const callExpressionNode = functionNode?.parent as + | TSESTree.CallExpression + | undefined; + + if (!callExpressionNode || !functionNode) { + return; + } + + const identifierNode = getDeepestIdentifierNode(callExpressionNode); + if (!identifierNode) { + return; + } + + if (!helpers.isActUtil(identifierNode)) { + return; + } + + if (isEmptyFunction(functionNode)) { + context.report({ + node: identifierNode, + messageId: 'noUnnecessaryActEmptyFunction', + }); + return; + } + + const shouldBeReported = isStrict + ? hasTestingLibraryCall(blockStatementNode.body) + : !hasSomeNonTestingLibraryCall(blockStatementNode.body); + + if (shouldBeReported) { + context.report({ + node: identifierNode, + messageId: 'noUnnecessaryActTestingLibraryUtil', + }); + } + } + + function checkNoUnnecessaryActFromImplicitReturn( + node: TSESTree.CallExpression + ) { + const nodeIdentifier = getDeepestIdentifierNode(node); + + if (!nodeIdentifier) { + return; + } + + const parentCallExpression = node.parent?.parent as + | TSESTree.CallExpression + | undefined; + + if (!parentCallExpression) { + return; + } + + const identifierNode = getDeepestIdentifierNode(parentCallExpression); + if (!identifierNode) { + return; + } + + if (!helpers.isActUtil(identifierNode)) { + return; + } + + if (!helpers.isTestingLibraryUtil(nodeIdentifier)) { + return; + } + + context.report({ + node: identifierNode, + messageId: 'noUnnecessaryActTestingLibraryUtil', + }); + } + + return { + 'CallExpression > ArrowFunctionExpression > BlockStatement': + checkNoUnnecessaryActFromBlockStatement, + 'CallExpression > FunctionExpression > BlockStatement': + checkNoUnnecessaryActFromBlockStatement, + 'CallExpression > ArrowFunctionExpression > CallExpression': + checkNoUnnecessaryActFromImplicitReturn, + }; + }, }); diff --git a/lib/rules/no-wait-for-empty-callback.ts b/lib/rules/no-wait-for-empty-callback.ts deleted file mode 100644 index 7bbfef99..00000000 --- a/lib/rules/no-wait-for-empty-callback.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; - -import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { - getPropertyIdentifierNode, - isCallExpression, - isEmptyFunction, -} from '../node-utils'; - -export const RULE_NAME = 'no-wait-for-empty-callback'; -export type MessageIds = 'noWaitForEmptyCallback'; -type Options = []; - -export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved`', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - noWaitForEmptyCallback: - 'Avoid passing empty callback to `{{ methodName }}`. Insert an assertion instead.', - }, - schema: [], - }, - defaultOptions: [], - - // trimmed down implementation of https://github.com/eslint/eslint/blob/master/lib/rules/no-empty-function.js - create(context, _, helpers) { - function isValidWaitFor(node: TSESTree.Node): boolean { - const parentCallExpression = node.parent as TSESTree.CallExpression; - const parentIdentifier = getPropertyIdentifierNode(parentCallExpression); - - if (!parentIdentifier) { - return false; - } - - return helpers.isAsyncUtil(parentIdentifier, [ - 'waitFor', - 'waitForElementToBeRemoved', - ]); - } - - function reportIfEmpty( - node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression - ) { - if (!isValidWaitFor(node)) { - return; - } - - if ( - isEmptyFunction(node) && - isCallExpression(node.parent) && - ASTUtils.isIdentifier(node.parent.callee) - ) { - context.report({ - node, - loc: node.body.loc.start, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: node.parent.callee.name, - }, - }); - } - } - - function reportNoop(node: TSESTree.Identifier) { - if (!isValidWaitFor(node)) { - return; - } - - context.report({ - node, - loc: node.loc.start, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: - isCallExpression(node.parent) && - ASTUtils.isIdentifier(node.parent.callee) && - node.parent.callee.name, - }, - }); - } - - return { - 'CallExpression > ArrowFunctionExpression': reportIfEmpty, - 'CallExpression > FunctionExpression': reportIfEmpty, - 'CallExpression > Identifier[name="noop"]': reportNoop, - }; - }, -}); diff --git a/lib/rules/no-wait-for-multiple-assertions.ts b/lib/rules/no-wait-for-multiple-assertions.ts index f3f23ebd..ed403068 100644 --- a/lib/rules/no-wait-for-multiple-assertions.ts +++ b/lib/rules/no-wait-for-multiple-assertions.ts @@ -1,91 +1,86 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { - getPropertyIdentifierNode, - isExpressionStatement, -} from '../node-utils'; +import { getPropertyIdentifierNode } from '../node-utils'; export const RULE_NAME = 'no-wait-for-multiple-assertions'; export type MessageIds = 'noWaitForMultipleAssertion'; type Options = []; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow the use of multiple `expect` calls inside `waitFor`', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - noWaitForMultipleAssertion: - 'Avoid using multiple assertions within `waitFor` callback', - }, - schema: [], - }, - defaultOptions: [], - create(context, _, helpers) { - function getExpectNodes( - body: Array - ): Array { - return body.filter((node) => { - if (!isExpressionStatement(node)) { - return false; - } + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: + 'Disallow the use of multiple `expect` calls inside `waitFor`', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + noWaitForMultipleAssertion: + 'Avoid using multiple assertions within `waitFor` callback', + }, + schema: [], + }, + defaultOptions: [], + create(context, _, helpers) { + function getExpectNodes( + body: Array + ): Array { + return body.filter((node) => { + const expressionIdentifier = getPropertyIdentifierNode(node); + if (!expressionIdentifier) { + return false; + } - const expressionIdentifier = getPropertyIdentifierNode(node); - if (!expressionIdentifier) { - return false; - } + return expressionIdentifier.name === 'expect'; + }) as Array; + } - return expressionIdentifier.name === 'expect'; - }) as Array; - } + function reportMultipleAssertion(node: TSESTree.BlockStatement) { + if (!node.parent) { + return; + } + const callExpressionNode = node.parent.parent as TSESTree.CallExpression; + const callExpressionIdentifier = + getPropertyIdentifierNode(callExpressionNode); - function reportMultipleAssertion(node: TSESTree.BlockStatement) { - if (!node.parent) { - return; - } - const callExpressionNode = node.parent.parent as TSESTree.CallExpression; - const callExpressionIdentifier = - getPropertyIdentifierNode(callExpressionNode); + if (!callExpressionIdentifier) { + return; + } - if (!callExpressionIdentifier) { - return; - } + if (!helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor'])) { + return; + } - if (!helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor'])) { - return; - } + const expectNodes = getExpectNodes(node.body); - const expectNodes = getExpectNodes(node.body); + if (expectNodes.length <= 1) { + return; + } - if (expectNodes.length <= 1) { - return; - } + for (let i = 0; i < expectNodes.length; i++) { + if (i !== 0) { + context.report({ + node: expectNodes[i], + messageId: 'noWaitForMultipleAssertion', + }); + } + } + } - for (let i = 0; i < expectNodes.length; i++) { - if (i !== 0) { - context.report({ - node: expectNodes[i], - messageId: 'noWaitForMultipleAssertion', - }); - } - } - } - - return { - 'CallExpression > ArrowFunctionExpression > BlockStatement': - reportMultipleAssertion, - 'CallExpression > FunctionExpression > BlockStatement': - reportMultipleAssertion, - }; - }, + return { + 'CallExpression > ArrowFunctionExpression > BlockStatement': + reportMultipleAssertion, + 'CallExpression > FunctionExpression > BlockStatement': + reportMultipleAssertion, + }; + }, }); diff --git a/lib/rules/no-wait-for-side-effects.ts b/lib/rules/no-wait-for-side-effects.ts index 4c919245..d2cf1aab 100644 --- a/lib/rules/no-wait-for-side-effects.ts +++ b/lib/rules/no-wait-for-side-effects.ts @@ -1,13 +1,14 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - getPropertyIdentifierNode, - isExpressionStatement, - isVariableDeclaration, - isAssignmentExpression, - isCallExpression, - isSequenceExpression, + getPropertyIdentifierNode, + isExpressionStatement, + isVariableDeclaration, + isAssignmentExpression, + isCallExpression, + isSequenceExpression, + hasThenProperty, } from '../node-utils'; export const RULE_NAME = 'no-wait-for-side-effects'; @@ -15,182 +16,245 @@ export type MessageIds = 'noSideEffectsWaitFor'; type Options = []; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: 'Disallow the use of side effects in `waitFor`', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - noSideEffectsWaitFor: - 'Avoid using side effects within `waitFor` callback', - }, - schema: [], - }, - defaultOptions: [], - create(context, _, helpers) { - function isCallerWaitFor( - node: - | TSESTree.AssignmentExpression - | TSESTree.BlockStatement - | TSESTree.CallExpression - | TSESTree.SequenceExpression - ): boolean { - if (!node.parent) { - return false; - } - const callExpressionNode = node.parent.parent as TSESTree.CallExpression; - const callExpressionIdentifier = - getPropertyIdentifierNode(callExpressionNode); - - return ( - !!callExpressionIdentifier && - helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor']) - ); - } - - function isRenderInVariableDeclaration(node: TSESTree.Node) { - return ( - isVariableDeclaration(node) && - node.declarations.some(helpers.isRenderVariableDeclarator) - ); - } - - function isRenderInExpressionStatement(node: TSESTree.Node) { - if ( - !isExpressionStatement(node) || - !isAssignmentExpression(node.expression) - ) { - return false; - } - - const expressionIdentifier = getPropertyIdentifierNode( - node.expression.right - ); - - if (!expressionIdentifier) { - return false; - } - - return helpers.isRenderUtil(expressionIdentifier); - } - - function isRenderInAssignmentExpression(node: TSESTree.Node) { - if (!isAssignmentExpression(node)) { - return false; - } - - const expressionIdentifier = getPropertyIdentifierNode(node.right); - if (!expressionIdentifier) { - return false; - } - - return helpers.isRenderUtil(expressionIdentifier); - } - - function isRenderInSequenceAssignment(node: TSESTree.Node) { - if (!isSequenceExpression(node)) { - return false; - } - - return node.expressions.some(isRenderInAssignmentExpression); - } - - function getSideEffectNodes( - body: TSESTree.Node[] - ): TSESTree.ExpressionStatement[] { - return body.filter((node) => { - if (!isExpressionStatement(node) && !isVariableDeclaration(node)) { - return false; - } - - if ( - isRenderInVariableDeclaration(node) || - isRenderInExpressionStatement(node) - ) { - return true; - } - - const expressionIdentifier = getPropertyIdentifierNode(node); - - if (!expressionIdentifier) { - return false; - } - - return ( - helpers.isFireEventUtil(expressionIdentifier) || - helpers.isUserEventUtil(expressionIdentifier) || - helpers.isRenderUtil(expressionIdentifier) - ); - }) as TSESTree.ExpressionStatement[]; - } - - function reportSideEffects(node: TSESTree.BlockStatement) { - if (!isCallerWaitFor(node)) { - return; - } - - getSideEffectNodes(node.body).forEach((sideEffectNode) => - context.report({ - node: sideEffectNode, - messageId: 'noSideEffectsWaitFor', - }) - ); - } - - function reportImplicitReturnSideEffect( - node: - | TSESTree.AssignmentExpression - | TSESTree.CallExpression - | TSESTree.SequenceExpression - ) { - if (!isCallerWaitFor(node)) { - return; - } - - const expressionIdentifier = isCallExpression(node) - ? getPropertyIdentifierNode(node.callee) - : null; - - if ( - !expressionIdentifier && - !isRenderInAssignmentExpression(node) && - !isRenderInSequenceAssignment(node) - ) { - return; - } - - if ( - expressionIdentifier && - !helpers.isFireEventUtil(expressionIdentifier) && - !helpers.isUserEventUtil(expressionIdentifier) && - !helpers.isRenderUtil(expressionIdentifier) - ) { - return; - } - - context.report({ - node, - messageId: 'noSideEffectsWaitFor', - }); - } - - return { - 'CallExpression > ArrowFunctionExpression > BlockStatement': - reportSideEffects, - 'CallExpression > ArrowFunctionExpression > CallExpression': - reportImplicitReturnSideEffect, - 'CallExpression > ArrowFunctionExpression > AssignmentExpression': - reportImplicitReturnSideEffect, - 'CallExpression > ArrowFunctionExpression > SequenceExpression': - reportImplicitReturnSideEffect, - 'CallExpression > FunctionExpression > BlockStatement': reportSideEffects, - }; - }, + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: 'Disallow the use of side effects in `waitFor`', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + noSideEffectsWaitFor: + 'Avoid using side effects within `waitFor` callback', + }, + schema: [], + }, + defaultOptions: [], + create(context, _, helpers) { + function isCallerWaitFor( + node: + | TSESTree.AssignmentExpression + | TSESTree.BlockStatement + | TSESTree.CallExpression + | TSESTree.SequenceExpression + ): boolean { + if (!node.parent) { + return false; + } + const callExpressionNode = node.parent.parent as TSESTree.CallExpression; + const callExpressionIdentifier = + getPropertyIdentifierNode(callExpressionNode); + + return ( + !!callExpressionIdentifier && + helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor']) + ); + } + + function isCallerThen( + node: + | TSESTree.AssignmentExpression + | TSESTree.BlockStatement + | TSESTree.CallExpression + | TSESTree.SequenceExpression + ): boolean { + if (!node.parent) { + return false; + } + + const callExpressionNode = node.parent.parent as TSESTree.CallExpression; + + return hasThenProperty(callExpressionNode.callee); + } + + function isRenderInVariableDeclaration(node: TSESTree.Node) { + return ( + isVariableDeclaration(node) && + node.declarations.some(helpers.isRenderVariableDeclarator) + ); + } + + function isRenderInExpressionStatement(node: TSESTree.Node) { + if ( + !isExpressionStatement(node) || + !isAssignmentExpression(node.expression) + ) { + return false; + } + + const expressionIdentifier = getPropertyIdentifierNode( + node.expression.right + ); + + if (!expressionIdentifier) { + return false; + } + + return helpers.isRenderUtil(expressionIdentifier); + } + + function isRenderInAssignmentExpression(node: TSESTree.Node) { + if (!isAssignmentExpression(node)) { + return false; + } + + const expressionIdentifier = getPropertyIdentifierNode(node.right); + if (!expressionIdentifier) { + return false; + } + + return helpers.isRenderUtil(expressionIdentifier); + } + + function isRenderInSequenceAssignment(node: TSESTree.Node) { + if (!isSequenceExpression(node)) { + return false; + } + + return node.expressions.some(isRenderInAssignmentExpression); + } + + /** + * Checks if there are side effects in variable declarations. + * + * For example, these variable declarations have side effects: + * const a = userEvent.doubleClick(button); + * const b = fireEvent.click(button); + * const wrapper = render(); + * + * @param node + * @returns {Boolean} Boolean indicating if variable declarataion has side effects + */ + function isSideEffectInVariableDeclaration( + node: TSESTree.VariableDeclaration + ): boolean { + return node.declarations.some((declaration) => { + if (isCallExpression(declaration.init)) { + const test = getPropertyIdentifierNode(declaration.init); + + if (!test) { + return false; + } + + return ( + helpers.isFireEventUtil(test) || + helpers.isUserEventUtil(test) || + helpers.isRenderUtil(test) + ); + } + return false; + }); + + return false; + } + + function getSideEffectNodes( + body: TSESTree.Node[] + ): TSESTree.ExpressionStatement[] { + return body.filter((node) => { + if (!isExpressionStatement(node) && !isVariableDeclaration(node)) { + return false; + } + + if ( + isRenderInVariableDeclaration(node) || + isRenderInExpressionStatement(node) + ) { + return true; + } + + if ( + isVariableDeclaration(node) && + isSideEffectInVariableDeclaration(node) + ) { + return true; + } + + const expressionIdentifier = getPropertyIdentifierNode(node); + + if (!expressionIdentifier) { + return false; + } + + return ( + helpers.isFireEventUtil(expressionIdentifier) || + helpers.isUserEventUtil(expressionIdentifier) || + helpers.isRenderUtil(expressionIdentifier) + ); + }) as TSESTree.ExpressionStatement[]; + } + + function reportSideEffects(node: TSESTree.BlockStatement) { + if (!isCallerWaitFor(node)) { + return; + } + + if (isCallerThen(node)) { + return; + } + + getSideEffectNodes(node.body).forEach((sideEffectNode) => + context.report({ + node: sideEffectNode, + messageId: 'noSideEffectsWaitFor', + }) + ); + } + + function reportImplicitReturnSideEffect( + node: + | TSESTree.AssignmentExpression + | TSESTree.CallExpression + | TSESTree.SequenceExpression + ) { + if (!isCallerWaitFor(node)) { + return; + } + + const expressionIdentifier = isCallExpression(node) + ? getPropertyIdentifierNode(node.callee) + : null; + + if ( + !expressionIdentifier && + !isRenderInAssignmentExpression(node) && + !isRenderInSequenceAssignment(node) + ) { + return; + } + + if ( + expressionIdentifier && + !helpers.isFireEventUtil(expressionIdentifier) && + !helpers.isUserEventUtil(expressionIdentifier) && + !helpers.isRenderUtil(expressionIdentifier) + ) { + return; + } + + context.report({ + node, + messageId: 'noSideEffectsWaitFor', + }); + } + + return { + 'CallExpression > ArrowFunctionExpression > BlockStatement': + reportSideEffects, + 'CallExpression > ArrowFunctionExpression > CallExpression': + reportImplicitReturnSideEffect, + 'CallExpression > ArrowFunctionExpression > AssignmentExpression': + reportImplicitReturnSideEffect, + 'CallExpression > ArrowFunctionExpression > SequenceExpression': + reportImplicitReturnSideEffect, + 'CallExpression > FunctionExpression > BlockStatement': reportSideEffects, + }; + }, }); diff --git a/lib/rules/no-wait-for-snapshot.ts b/lib/rules/no-wait-for-snapshot.ts index 96450678..c7c41386 100644 --- a/lib/rules/no-wait-for-snapshot.ts +++ b/lib/rules/no-wait-for-snapshot.ts @@ -1,9 +1,9 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - findClosestCallExpressionNode, - isMemberExpression, + findClosestCallExpressionNode, + isMemberExpression, } from '../node-utils'; export const RULE_NAME = 'no-wait-for-snapshot'; @@ -13,71 +13,75 @@ type Options = []; const SNAPSHOT_REGEXP = /^(toMatchSnapshot|toMatchInlineSnapshot)$/; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: - 'Ensures no snapshot is generated inside of a `waitFor` call', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - noWaitForSnapshot: - "A snapshot can't be generated inside of a `{{ name }}` call", - }, - schema: [], - }, - defaultOptions: [], + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: + 'Ensures no snapshot is generated inside of a `waitFor` call', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + noWaitForSnapshot: + "A snapshot can't be generated inside of a `{{ name }}` call", + }, + schema: [], + }, + defaultOptions: [], - create(context, _, helpers) { - function getClosestAsyncUtil( - node: TSESTree.Node - ): TSESTree.Identifier | null { - let n: TSESTree.Node | null = node; - do { - const callExpression = findClosestCallExpressionNode(n); + create(context, _, helpers) { + function getClosestAsyncUtil( + node: TSESTree.Node + ): TSESTree.Identifier | null { + let n: TSESTree.Node | null = node; + do { + const callExpression = findClosestCallExpressionNode(n); - if (!callExpression) { - return null; - } + if (!callExpression) { + return null; + } - if ( - ASTUtils.isIdentifier(callExpression.callee) && - helpers.isAsyncUtil(callExpression.callee) - ) { - return callExpression.callee; - } - if ( - isMemberExpression(callExpression.callee) && - ASTUtils.isIdentifier(callExpression.callee.property) && - helpers.isAsyncUtil(callExpression.callee.property) - ) { - return callExpression.callee.property; - } - if (callExpression.parent) { - n = findClosestCallExpressionNode(callExpression.parent); - } - } while (n !== null); - return null; - } + if ( + ASTUtils.isIdentifier(callExpression.callee) && + helpers.isAsyncUtil(callExpression.callee) + ) { + return callExpression.callee; + } + if ( + isMemberExpression(callExpression.callee) && + ASTUtils.isIdentifier(callExpression.callee.property) && + helpers.isAsyncUtil(callExpression.callee.property) + ) { + return callExpression.callee.property; + } + if (callExpression.parent) { + n = findClosestCallExpressionNode(callExpression.parent); + } + } while (n !== null); + return null; + } - return { - [`Identifier[name=${SNAPSHOT_REGEXP}]`](node: TSESTree.Identifier) { - const closestAsyncUtil = getClosestAsyncUtil(node); - if (closestAsyncUtil === null) { - return; - } - context.report({ - node, - messageId: 'noWaitForSnapshot', - data: { name: closestAsyncUtil.name }, - }); - }, - }; - }, + return { + [`Identifier[name=${String(SNAPSHOT_REGEXP)}]`]( + node: TSESTree.Identifier + ) { + const closestAsyncUtil = getClosestAsyncUtil(node); + if (closestAsyncUtil === null) { + return; + } + context.report({ + node, + messageId: 'noWaitForSnapshot', + data: { name: closestAsyncUtil.name }, + }); + }, + }; + }, }); diff --git a/lib/rules/prefer-explicit-assert.ts b/lib/rules/prefer-explicit-assert.ts index a8d376f0..ed0ffc8c 100644 --- a/lib/rules/prefer-explicit-assert.ts +++ b/lib/rules/prefer-explicit-assert.ts @@ -1,199 +1,208 @@ -import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; +import { TSESTree, ASTUtils } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - findClosestCallNode, - isCallExpression, - isMemberExpression, + findClosestCallNode, + isCallExpression, + isMemberExpression, } from '../node-utils'; import { PRESENCE_MATCHERS, ABSENCE_MATCHERS } from '../utils'; export const RULE_NAME = 'prefer-explicit-assert'; export type MessageIds = - | 'preferExplicitAssert' - | 'preferExplicitAssertAssertion'; + | 'preferExplicitAssert' + | 'preferExplicitAssertAssertion'; type Options = [ - { - assertion?: string; - includeFindQueries?: boolean; - } + { + assertion?: string; + includeFindQueries?: boolean; + }, ]; const isAtTopLevel = (node: TSESTree.Node) => - (!!node.parent?.parent && - node.parent.parent.type === 'ExpressionStatement') || - (node.parent?.parent?.type === 'AwaitExpression' && - !!node.parent.parent.parent && - node.parent.parent.parent.type === 'ExpressionStatement'); + (!!node.parent?.parent && + node.parent.parent.type === TSESTree.AST_NODE_TYPES.ExpressionStatement) || + (node.parent?.parent?.type === TSESTree.AST_NODE_TYPES.AwaitExpression && + !!node.parent.parent.parent && + node.parent.parent.parent.type === + TSESTree.AST_NODE_TYPES.ExpressionStatement); const isVariableDeclaration = (node: TSESTree.Node) => { - if ( - isCallExpression(node.parent) && - ASTUtils.isAwaitExpression(node.parent.parent) && - ASTUtils.isVariableDeclarator(node.parent.parent.parent) - ) { - return true; // const quxElement = await findByLabelText('qux') - } - - if ( - isCallExpression(node.parent) && - ASTUtils.isVariableDeclarator(node.parent.parent) - ) { - return true; // const quxElement = findByLabelText('qux') - } - - if ( - isMemberExpression(node.parent) && - isCallExpression(node.parent.parent) && - ASTUtils.isAwaitExpression(node.parent.parent.parent) && - ASTUtils.isVariableDeclarator(node.parent.parent.parent.parent) - ) { - return true; // const quxElement = await screen.findByLabelText('qux') - } - - if ( - isMemberExpression(node.parent) && - isCallExpression(node.parent.parent) && - ASTUtils.isVariableDeclarator(node.parent.parent.parent) - ) { - return true; // const quxElement = screen.findByLabelText('qux') - } - - return false; + if ( + isCallExpression(node.parent) && + ASTUtils.isAwaitExpression(node.parent.parent) && + ASTUtils.isVariableDeclarator(node.parent.parent.parent) + ) { + return true; // const quxElement = await findByLabelText('qux') + } + + if ( + isCallExpression(node.parent) && + ASTUtils.isVariableDeclarator(node.parent.parent) + ) { + return true; // const quxElement = findByLabelText('qux') + } + + if ( + isMemberExpression(node.parent) && + isCallExpression(node.parent.parent) && + ASTUtils.isAwaitExpression(node.parent.parent.parent) && + ASTUtils.isVariableDeclarator(node.parent.parent.parent.parent) + ) { + return true; // const quxElement = await screen.findByLabelText('qux') + } + + if ( + isMemberExpression(node.parent) && + isCallExpression(node.parent.parent) && + ASTUtils.isVariableDeclarator(node.parent.parent.parent) + ) { + return true; // const quxElement = screen.findByLabelText('qux') + } + + return false; }; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: - 'Suggest using explicit assertions rather than standalone queries', - recommendedConfig: { - dom: false, - angular: false, - react: false, - vue: false, - }, - }, - messages: { - preferExplicitAssert: - 'Wrap stand-alone `{{queryType}}` query with `expect` function for better explicit assertion', - preferExplicitAssertAssertion: - '`getBy*` queries must be asserted with `{{assertion}}`', // TODO: support findBy* queries as well - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - assertion: { - type: 'string', - enum: PRESENCE_MATCHERS, - }, - includeFindQueries: { type: 'boolean' }, - }, - }, - ], - }, - defaultOptions: [{ includeFindQueries: true }], - create(context, [options], helpers) { - const { assertion, includeFindQueries } = options; - const getQueryCalls: TSESTree.Identifier[] = []; - const findQueryCalls: TSESTree.Identifier[] = []; - - return { - 'CallExpression Identifier'(node: TSESTree.Identifier) { - if (helpers.isGetQueryVariant(node)) { - getQueryCalls.push(node); - } - - if (helpers.isFindQueryVariant(node)) { - findQueryCalls.push(node); - } - }, - 'Program:exit'() { - if (includeFindQueries) { - findQueryCalls.forEach((queryCall) => { - const memberExpression = isMemberExpression(queryCall.parent) - ? queryCall.parent - : queryCall; - - if ( - isVariableDeclaration(queryCall) || - !isAtTopLevel(memberExpression) - ) { - return; - } - - context.report({ - node: queryCall, - messageId: 'preferExplicitAssert', - data: { - queryType: 'findBy*', - }, - }); - }); - } - - getQueryCalls.forEach((queryCall) => { - const node = isMemberExpression(queryCall.parent) - ? queryCall.parent - : queryCall; - - if (isAtTopLevel(node)) { - context.report({ - node: queryCall, - messageId: 'preferExplicitAssert', - data: { - queryType: 'getBy*', - }, - }); - } - - if (assertion) { - const expectCallNode = findClosestCallNode(node, 'expect'); - if (!expectCallNode) return; - - const expectStatement = expectCallNode.parent; - if (!isMemberExpression(expectStatement)) { - return; - } - - const property = expectStatement.property; - - if (!ASTUtils.isIdentifier(property)) { - return; - } - - let matcher = property.name; - let isNegatedMatcher = false; - - if ( - matcher === 'not' && - isMemberExpression(expectStatement.parent) && - ASTUtils.isIdentifier(expectStatement.parent.property) - ) { - isNegatedMatcher = true; - matcher = expectStatement.parent.property.name; - } - - const shouldEnforceAssertion = - (!isNegatedMatcher && PRESENCE_MATCHERS.includes(matcher)) || - (isNegatedMatcher && ABSENCE_MATCHERS.includes(matcher)); - - if (shouldEnforceAssertion && matcher !== assertion) { - context.report({ - node: property, - messageId: 'preferExplicitAssertAssertion', - data: { - assertion, - }, - }); - } - } - }); - }, - }; - }, + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: + 'Suggest using explicit assertions rather than standalone queries', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + svelte: false, + marko: false, + }, + }, + messages: { + preferExplicitAssert: + 'Wrap stand-alone `{{queryType}}` query with `expect` function for better explicit assertion', + preferExplicitAssertAssertion: + '`getBy*` queries must be asserted with `{{assertion}}`', // TODO: support findBy* queries as well + }, + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + assertion: { + type: 'string', + enum: [...PRESENCE_MATCHERS], + }, + includeFindQueries: { type: 'boolean' }, + }, + }, + ], + }, + defaultOptions: [{ includeFindQueries: true }], + create(context, [options], helpers) { + const { assertion, includeFindQueries } = options; + const getQueryCalls: TSESTree.Identifier[] = []; + const findQueryCalls: TSESTree.Identifier[] = []; + + return { + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if (helpers.isGetQueryVariant(node)) { + getQueryCalls.push(node); + } + + if (helpers.isFindQueryVariant(node)) { + findQueryCalls.push(node); + } + }, + 'Program:exit'() { + if (includeFindQueries) { + findQueryCalls.forEach((queryCall) => { + const memberExpression = isMemberExpression(queryCall.parent) + ? queryCall.parent + : queryCall; + + if ( + isVariableDeclaration(queryCall) || + !isAtTopLevel(memberExpression) + ) { + return; + } + + context.report({ + node: queryCall, + messageId: 'preferExplicitAssert', + data: { + queryType: 'findBy*', + }, + }); + }); + } + + getQueryCalls.forEach((queryCall) => { + const node = isMemberExpression(queryCall.parent) + ? queryCall.parent + : queryCall; + + if (isAtTopLevel(node)) { + context.report({ + node: queryCall, + messageId: 'preferExplicitAssert', + data: { + queryType: 'getBy*', + }, + }); + } + + if (assertion) { + const expectCallNode = findClosestCallNode(node, 'expect'); + if (!expectCallNode) return; + + const expectStatement = expectCallNode.parent; + if (!isMemberExpression(expectStatement)) { + return; + } + + const property = expectStatement.property; + + if (!ASTUtils.isIdentifier(property)) { + return; + } + + let matcher = property.name; + let isNegatedMatcher = false; + + if ( + matcher === 'not' && + isMemberExpression(expectStatement.parent) && + ASTUtils.isIdentifier(expectStatement.parent.property) + ) { + isNegatedMatcher = true; + matcher = expectStatement.parent.property.name; + } + + const shouldEnforceAssertion = + (!isNegatedMatcher && + PRESENCE_MATCHERS.some( + (presenceMather) => presenceMather === matcher + )) || + (isNegatedMatcher && + ABSENCE_MATCHERS.some( + (absenceMather) => absenceMather === matcher + )); + + if (shouldEnforceAssertion && matcher !== assertion) { + context.report({ + node: property, + messageId: 'preferExplicitAssertAssertion', + data: { + assertion, + }, + }); + } + } + }); + }, + }; + }, }); diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 460eab38..5e3f35a7 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -1,479 +1,549 @@ -import { - TSESTree, - ASTUtils, - TSESLint, -} from '@typescript-eslint/experimental-utils'; +import { TSESTree, ASTUtils, TSESLint } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - isArrowFunctionExpression, - isCallExpression, - isMemberExpression, - isObjectPattern, - isProperty, + getDeepestIdentifierNode, + isArrowFunctionExpression, + isBlockStatement, + isCallExpression, + isMemberExpression, + isObjectExpression, + isObjectPattern, + isProperty, + isVariableDeclaration, } from '../node-utils'; +import { getScope, getSourceCode } from '../utils'; export const RULE_NAME = 'prefer-find-by'; export type MessageIds = 'preferFindBy'; type Options = []; -export const WAIT_METHODS = ['waitFor', 'waitForElement', 'wait'] as const; - export function getFindByQueryVariant( - queryMethod: string + queryMethod: string ): 'findAllBy' | 'findBy' { - return queryMethod.includes('All') ? 'findAllBy' : 'findBy'; + return queryMethod.includes('All') ? 'findAllBy' : 'findBy'; } function findRenderDefinitionDeclaration( - scope: TSESLint.Scope.Scope | null, - query: string + scope: TSESLint.Scope.Scope | null, + query: string ): TSESTree.Identifier | null { - if (!scope) { - return null; - } - - const variable = scope.variables.find( - (v: TSESLint.Scope.Variable) => v.name === query - ); - - if (variable) { - return ( - variable.defs - .map(({ name }) => name) - .filter(ASTUtils.isIdentifier) - .find(({ name }) => name === query) ?? null - ); - } - - return findRenderDefinitionDeclaration(scope.upper, query); + if (!scope) { + return null; + } + + const variable = scope.variables.find( + (v: TSESLint.Scope.Variable) => v.name === query + ); + + if (variable) { + return ( + variable.defs + .map(({ name }) => name) + .filter(ASTUtils.isIdentifier) + .find(({ name }) => name === query) ?? null + ); + } + + return findRenderDefinitionDeclaration(scope.upper, query); } export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: - 'Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - preferFindBy: - 'Prefer `{{queryVariant}}{{queryMethod}}` query over using `{{waitForMethodName}}` + `{{prevQuery}}`', - }, - fixable: 'code', - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - const sourceCode = context.getSourceCode(); - - /** - * Reports the invalid usage of wait* plus getBy/QueryBy methods and automatically fixes the scenario - * @param node - The CallExpresion node that contains the wait* method - * @param replacementParams - Object with info for error message and autofix: - * @param replacementParams.queryVariant - The variant method used to query: findBy/findAllBy. - * @param replacementParams.prevQuery - The query originally used inside `waitFor` - * @param replacementParams.queryMethod - Suffix string to build the query method (the query-part that comes after the "By"): LabelText, Placeholder, Text, Role, Title, etc. - * @param replacementParams.waitForMethodName - wait for method used: waitFor/wait/waitForElement - * @param replacementParams.fix - Function that applies the fix to correct the code - */ - function reportInvalidUsage( - node: TSESTree.CallExpression, - replacementParams: { - queryVariant: 'findAllBy' | 'findBy'; - queryMethod: string; - prevQuery: string; - waitForMethodName: string; - fix: TSESLint.ReportFixFunction; - } - ) { - const { queryMethod, queryVariant, prevQuery, waitForMethodName, fix } = - replacementParams; - context.report({ - node, - messageId: 'preferFindBy', - data: { - queryVariant, - queryMethod, - prevQuery, - waitForMethodName, - }, - fix, - }); - } - - function getWrongQueryNameInAssertion( - node: TSESTree.ArrowFunctionExpression - ) { - if ( - !isCallExpression(node.body) || - !isMemberExpression(node.body.callee) - ) { - return null; - } - - // expect(getByText).toBeInTheDocument() shape - if ( - isCallExpression(node.body.callee.object) && - isCallExpression(node.body.callee.object.arguments[0]) && - ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee) - ) { - return node.body.callee.object.arguments[0].callee.name; - } - - if (!ASTUtils.isIdentifier(node.body.callee.property)) { - return null; - } - - // expect(screen.getByText).toBeInTheDocument() shape - if ( - isCallExpression(node.body.callee.object) && - isCallExpression(node.body.callee.object.arguments[0]) && - isMemberExpression(node.body.callee.object.arguments[0].callee) && - ASTUtils.isIdentifier( - node.body.callee.object.arguments[0].callee.property - ) - ) { - return node.body.callee.object.arguments[0].callee.property.name; - } - - // expect(screen.getByText).not shape - if ( - isMemberExpression(node.body.callee.object) && - isCallExpression(node.body.callee.object.object) && - isCallExpression(node.body.callee.object.object.arguments[0]) && - isMemberExpression( - node.body.callee.object.object.arguments[0].callee - ) && - ASTUtils.isIdentifier( - node.body.callee.object.object.arguments[0].callee.property - ) - ) { - return node.body.callee.object.object.arguments[0].callee.property.name; - } - - // expect(getByText).not shape - if ( - isMemberExpression(node.body.callee.object) && - isCallExpression(node.body.callee.object.object) && - isCallExpression(node.body.callee.object.object.arguments[0]) && - ASTUtils.isIdentifier( - node.body.callee.object.object.arguments[0].callee - ) - ) { - return node.body.callee.object.object.arguments[0].callee.name; - } - - return node.body.callee.property.name; - } - - function getWrongQueryName(node: TSESTree.ArrowFunctionExpression) { - if (!isCallExpression(node.body)) { - return null; - } - - // expect(() => getByText) and expect(() => screen.getByText) shape - if ( - ASTUtils.isIdentifier(node.body.callee) && - helpers.isSyncQuery(node.body.callee) - ) { - return node.body.callee.name; - } - - return getWrongQueryNameInAssertion(node); - } - - function getCaller(node: TSESTree.ArrowFunctionExpression) { - if ( - !isCallExpression(node.body) || - !isMemberExpression(node.body.callee) - ) { - return null; - } - - if (ASTUtils.isIdentifier(node.body.callee.object)) { - // () => screen.getByText - return node.body.callee.object.name; - } - - if ( - // expect() - isCallExpression(node.body.callee.object) && - ASTUtils.isIdentifier(node.body.callee.object.callee) && - isCallExpression(node.body.callee.object.arguments[0]) && - isMemberExpression(node.body.callee.object.arguments[0].callee) && - ASTUtils.isIdentifier( - node.body.callee.object.arguments[0].callee.object - ) - ) { - return node.body.callee.object.arguments[0].callee.object.name; - } - - if ( - // expect().not - isMemberExpression(node.body.callee.object) && - isCallExpression(node.body.callee.object.object) && - isCallExpression(node.body.callee.object.object.arguments[0]) && - isMemberExpression( - node.body.callee.object.object.arguments[0].callee - ) && - ASTUtils.isIdentifier( - node.body.callee.object.object.arguments[0].callee.object - ) - ) { - return node.body.callee.object.object.arguments[0].callee.object.name; - } - - return null; - } - - function isSyncQuery(node: TSESTree.ArrowFunctionExpression) { - if (!isCallExpression(node.body)) { - return false; - } - - const isQuery = - ASTUtils.isIdentifier(node.body.callee) && - helpers.isSyncQuery(node.body.callee); - - const isWrappedInPresenceAssert = - isMemberExpression(node.body.callee) && - isCallExpression(node.body.callee.object) && - isCallExpression(node.body.callee.object.arguments[0]) && - ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee) && - helpers.isSyncQuery(node.body.callee.object.arguments[0].callee) && - helpers.isPresenceAssert(node.body.callee); - - const isWrappedInNegatedPresenceAssert = - isMemberExpression(node.body.callee) && - isMemberExpression(node.body.callee.object) && - isCallExpression(node.body.callee.object.object) && - isCallExpression(node.body.callee.object.object.arguments[0]) && - ASTUtils.isIdentifier( - node.body.callee.object.object.arguments[0].callee - ) && - helpers.isSyncQuery( - node.body.callee.object.object.arguments[0].callee - ) && - helpers.isPresenceAssert(node.body.callee.object); - - return ( - isQuery || isWrappedInPresenceAssert || isWrappedInNegatedPresenceAssert - ); - } - - function isScreenSyncQuery(node: TSESTree.ArrowFunctionExpression) { - if (!isArrowFunctionExpression(node) || !isCallExpression(node.body)) { - return false; - } - - if ( - !isMemberExpression(node.body.callee) || - !ASTUtils.isIdentifier(node.body.callee.property) - ) { - return false; - } - - if ( - !ASTUtils.isIdentifier(node.body.callee.object) && - !isCallExpression(node.body.callee.object) && - !isMemberExpression(node.body.callee.object) - ) { - return false; - } - - const isWrappedInPresenceAssert = - helpers.isPresenceAssert(node.body.callee) && - isCallExpression(node.body.callee.object) && - isCallExpression(node.body.callee.object.arguments[0]) && - isMemberExpression(node.body.callee.object.arguments[0].callee) && - ASTUtils.isIdentifier( - node.body.callee.object.arguments[0].callee.object - ); - - const isWrappedInNegatedPresenceAssert = - isMemberExpression(node.body.callee.object) && - helpers.isPresenceAssert(node.body.callee.object) && - isCallExpression(node.body.callee.object.object) && - isCallExpression(node.body.callee.object.object.arguments[0]) && - isMemberExpression(node.body.callee.object.object.arguments[0].callee); - - return ( - helpers.isSyncQuery(node.body.callee.property) || - isWrappedInPresenceAssert || - isWrappedInNegatedPresenceAssert - ); - } - - function getQueryArguments(node: TSESTree.CallExpression) { - if ( - isMemberExpression(node.callee) && - isCallExpression(node.callee.object) && - isCallExpression(node.callee.object.arguments[0]) - ) { - return node.callee.object.arguments[0].arguments; - } - - if ( - isMemberExpression(node.callee) && - isMemberExpression(node.callee.object) && - isCallExpression(node.callee.object.object) && - isCallExpression(node.callee.object.object.arguments[0]) - ) { - return node.callee.object.object.arguments[0].arguments; - } - - return node.arguments; - } - - return { - 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { - if ( - !ASTUtils.isIdentifier(node.callee) || - !helpers.isAsyncUtil(node.callee, WAIT_METHODS) - ) { - return; - } - // ensure the only argument is an arrow function expression - if the arrow function is a block - // we skip it - const argument = node.arguments[0]; - if ( - !isArrowFunctionExpression(argument) || - !isCallExpression(argument.body) - ) { - return; - } - - const waitForMethodName = node.callee.name; - - // ensure here it's one of the sync methods that we are calling - if (isScreenSyncQuery(argument)) { - const caller = getCaller(argument); - - if (!caller) { - return; - } - - // shape of () => screen.getByText - const fullQueryMethod = getWrongQueryName(argument); - - if (!fullQueryMethod) { - return; - } - - const queryVariant = getFindByQueryVariant(fullQueryMethod); - const callArguments = getQueryArguments(argument.body); - const queryMethod = fullQueryMethod.split('By')[1]; - - reportInvalidUsage(node, { - queryMethod, - queryVariant, - prevQuery: fullQueryMethod, - waitForMethodName, - fix(fixer) { - const property = ( - (argument.body as TSESTree.CallExpression) - .callee as TSESTree.MemberExpression - ).property; - if (helpers.isCustomQuery(property as TSESTree.Identifier)) { - return null; - } - const newCode = `${caller}.${queryVariant}${queryMethod}(${callArguments - .map((callArgNode) => sourceCode.getText(callArgNode)) - .join(', ')})`; - return fixer.replaceText(node, newCode); - }, - }); - return; - } - - if (!isSyncQuery(argument)) { - return; - } - - // shape of () => getByText - const fullQueryMethod = getWrongQueryName(argument); - - if (!fullQueryMethod) { - return; - } - - const queryMethod = fullQueryMethod.split('By')[1]; - const queryVariant = getFindByQueryVariant(fullQueryMethod); - const callArguments = getQueryArguments(argument.body); - - reportInvalidUsage(node, { - queryMethod, - queryVariant, - prevQuery: fullQueryMethod, - waitForMethodName, - fix(fixer) { - // we know from above callee is an Identifier - if ( - helpers.isCustomQuery( - (argument.body as TSESTree.CallExpression) - .callee as TSESTree.Identifier - ) - ) { - return null; - } - const findByMethod = `${queryVariant}${queryMethod}`; - const allFixes: TSESLint.RuleFix[] = []; - // this updates waitFor with findBy* - const newCode = `${findByMethod}(${callArguments - .map((callArgNode) => sourceCode.getText(callArgNode)) - .join(', ')})`; - allFixes.push(fixer.replaceText(node, newCode)); - - // this adds the findBy* declaration - adding it to the list of destructured variables { findBy* } = render() - const definition = findRenderDefinitionDeclaration( - context.getScope(), - fullQueryMethod - ); - // I think it should always find it, otherwise code should not be valid (it'd be using undeclared variables) - if (!definition) { - return allFixes; - } - // check the declaration is part of a destructuring - if ( - definition.parent && - isObjectPattern(definition.parent.parent) - ) { - const allVariableDeclarations = definition.parent.parent; - // verify if the findBy* method was already declared - if ( - allVariableDeclarations.properties.some( - (p) => - isProperty(p) && - ASTUtils.isIdentifier(p.key) && - p.key.name === findByMethod - ) - ) { - return allFixes; - } - // the last character of a destructuring is always a "}", so we should replace it with the findBy* declaration - const textDestructuring = sourceCode.getText( - allVariableDeclarations - ); - const text = textDestructuring.replace( - /(\s*})$/, - `, ${findByMethod}$1` - ); - allFixes.push(fixer.replaceText(allVariableDeclarations, text)); - } - - return allFixes; - }, - }); - }, - }; - }, + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: + 'Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + preferFindBy: + 'Prefer `{{queryVariant}}{{queryMethod}}` query over using `waitFor` + `{{prevQuery}}`', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + + create(context, _, helpers) { + const sourceCode = getSourceCode(context); + + /** + * Reports the invalid usage of wait* plus getBy/QueryBy methods and automatically fixes the scenario + * @param node - The CallExpression node that contains the waitFor method + * @param replacementParams - Object with info for error message and autofix: + * @param replacementParams.queryVariant - The variant method used to query: findBy/findAllBy. + * @param replacementParams.prevQuery - The query originally used inside `waitFor` + * @param replacementParams.queryMethod - Suffix string to build the query method (the query-part that comes after the "By"): LabelText, Placeholder, Text, Role, Title, etc. + * @param replacementParams.fix - Function that applies the fix to correct the code + */ + function reportInvalidUsage( + node: TSESTree.CallExpression, + replacementParams: { + queryVariant: 'findAllBy' | 'findBy'; + queryMethod: string; + prevQuery: string; + fix: TSESLint.ReportFixFunction; + } + ) { + const { queryMethod, queryVariant, prevQuery, fix } = replacementParams; + context.report({ + node, + messageId: 'preferFindBy', + data: { + queryVariant, + queryMethod, + prevQuery, + }, + fix, + }); + } + + function getWrongQueryNameInAssertion( + node: TSESTree.ArrowFunctionExpression + ) { + if ( + !isCallExpression(node.body) || + !isMemberExpression(node.body.callee) + ) { + return null; + } + + // expect(getByText).toBeInTheDocument() shape + if ( + isCallExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.arguments[0]) && + ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee) + ) { + return node.body.callee.object.arguments[0].callee; + } + + if (!ASTUtils.isIdentifier(node.body.callee.property)) { + return null; + } + + // expect(screen.getByText).toBeInTheDocument() shape + if ( + isCallExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.arguments[0]) && + isMemberExpression(node.body.callee.object.arguments[0].callee) && + ASTUtils.isIdentifier( + node.body.callee.object.arguments[0].callee.property + ) + ) { + return node.body.callee.object.arguments[0].callee.property; + } + + // expect(screen.getByText).not shape + if ( + isMemberExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.object) && + isCallExpression(node.body.callee.object.object.arguments[0]) && + isMemberExpression( + node.body.callee.object.object.arguments[0].callee + ) && + ASTUtils.isIdentifier( + node.body.callee.object.object.arguments[0].callee.property + ) + ) { + return node.body.callee.object.object.arguments[0].callee.property; + } + + // expect(getByText).not shape + if ( + isMemberExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.object) && + isCallExpression(node.body.callee.object.object.arguments[0]) && + ASTUtils.isIdentifier( + node.body.callee.object.object.arguments[0].callee + ) + ) { + return node.body.callee.object.object.arguments[0].callee; + } + + return node.body.callee.property; + } + + function getWrongQueryName(node: TSESTree.ArrowFunctionExpression) { + if (!isCallExpression(node.body)) { + return null; + } + + // expect(() => getByText) and expect(() => screen.getByText) shape + if ( + ASTUtils.isIdentifier(node.body.callee) && + helpers.isSyncQuery(node.body.callee) + ) { + return node.body.callee; + } + + return getWrongQueryNameInAssertion(node); + } + + function getCaller(node: TSESTree.ArrowFunctionExpression) { + if ( + !isCallExpression(node.body) || + !isMemberExpression(node.body.callee) + ) { + return null; + } + + if (ASTUtils.isIdentifier(node.body.callee.object)) { + // () => screen.getByText + return node.body.callee.object.name; + } + + if ( + // expect() + isCallExpression(node.body.callee.object) && + ASTUtils.isIdentifier(node.body.callee.object.callee) && + isCallExpression(node.body.callee.object.arguments[0]) && + isMemberExpression(node.body.callee.object.arguments[0].callee) && + ASTUtils.isIdentifier( + node.body.callee.object.arguments[0].callee.object + ) + ) { + return node.body.callee.object.arguments[0].callee.object.name; + } + + if ( + // expect().not + isMemberExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.object) && + isCallExpression(node.body.callee.object.object.arguments[0]) && + isMemberExpression( + node.body.callee.object.object.arguments[0].callee + ) && + ASTUtils.isIdentifier( + node.body.callee.object.object.arguments[0].callee.object + ) + ) { + return node.body.callee.object.object.arguments[0].callee.object.name; + } + + return null; + } + + function isSyncQuery(node: TSESTree.ArrowFunctionExpression) { + if (!isCallExpression(node.body)) { + return false; + } + + const isQuery = + ASTUtils.isIdentifier(node.body.callee) && + helpers.isSyncQuery(node.body.callee); + + const isWrappedInPresenceAssert = + isMemberExpression(node.body.callee) && + isCallExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.arguments[0]) && + ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee) && + helpers.isSyncQuery(node.body.callee.object.arguments[0].callee) && + helpers.isPresenceAssert(node.body.callee); + + const isWrappedInNegatedPresenceAssert = + isMemberExpression(node.body.callee) && + isMemberExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.object) && + isCallExpression(node.body.callee.object.object.arguments[0]) && + ASTUtils.isIdentifier( + node.body.callee.object.object.arguments[0].callee + ) && + helpers.isSyncQuery( + node.body.callee.object.object.arguments[0].callee + ) && + helpers.isPresenceAssert(node.body.callee.object); + + return ( + isQuery || isWrappedInPresenceAssert || isWrappedInNegatedPresenceAssert + ); + } + + function isScreenSyncQuery(node: TSESTree.ArrowFunctionExpression) { + if (!isArrowFunctionExpression(node) || !isCallExpression(node.body)) { + return false; + } + + if ( + !isMemberExpression(node.body.callee) || + !ASTUtils.isIdentifier(node.body.callee.property) + ) { + return false; + } + + if ( + !ASTUtils.isIdentifier(node.body.callee.object) && + !isCallExpression(node.body.callee.object) && + !isMemberExpression(node.body.callee.object) + ) { + return false; + } + + const isWrappedInPresenceAssert = + helpers.isPresenceAssert(node.body.callee) && + isCallExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.arguments[0]) && + isMemberExpression(node.body.callee.object.arguments[0].callee) && + ASTUtils.isIdentifier( + node.body.callee.object.arguments[0].callee.object + ); + + const isWrappedInNegatedPresenceAssert = + isMemberExpression(node.body.callee.object) && + helpers.isPresenceAssert(node.body.callee.object) && + isCallExpression(node.body.callee.object.object) && + isCallExpression(node.body.callee.object.object.arguments[0]) && + isMemberExpression(node.body.callee.object.object.arguments[0].callee); + + return ( + helpers.isSyncQuery(node.body.callee.property) || + isWrappedInPresenceAssert || + isWrappedInNegatedPresenceAssert + ); + } + + function getQueryArguments(node: TSESTree.CallExpression) { + if ( + isMemberExpression(node.callee) && + isCallExpression(node.callee.object) && + isCallExpression(node.callee.object.arguments[0]) + ) { + return node.callee.object.arguments[0].arguments; + } + + if ( + isMemberExpression(node.callee) && + isMemberExpression(node.callee.object) && + isCallExpression(node.callee.object.object) && + isCallExpression(node.callee.object.object.arguments[0]) + ) { + return node.callee.object.object.arguments[0].arguments; + } + + return node.arguments; + } + + return { + 'AwaitExpression > CallExpression'( + node: TSESTree.CallExpression & { parent: TSESTree.AwaitExpression } + ) { + if ( + !ASTUtils.isIdentifier(node.callee) || + !helpers.isAsyncUtil(node.callee, ['waitFor']) + ) { + return; + } + // ensure the only argument is an arrow function expression + const argument = node.arguments[0]; + + if (!isArrowFunctionExpression(argument)) { + return; + } + + if (isBlockStatement(argument.body) && argument.async) { + const { body } = argument.body; + const declarations = body + .filter(isVariableDeclaration) + ?.flatMap((declaration) => declaration.declarations); + + const findByDeclarator = declarations.find((declaration) => { + if ( + !ASTUtils.isAwaitExpression(declaration.init) || + !isCallExpression(declaration.init.argument) + ) { + return false; + } + + const { callee } = declaration.init.argument; + const node = getDeepestIdentifierNode(callee); + return node ? helpers.isFindQueryVariant(node) : false; + }); + + const init = ASTUtils.isAwaitExpression(findByDeclarator?.init) + ? findByDeclarator.init.argument + : null; + + if (!isCallExpression(init)) { + return; + } + const queryIdentifier = getDeepestIdentifierNode(init.callee); + + // ensure the query is a supported async query like findBy* + if (!queryIdentifier || !helpers.isAsyncQuery(queryIdentifier)) { + return; + } + + const fullQueryMethod = queryIdentifier.name; + const queryMethod = fullQueryMethod.split('By')[1]; + const queryVariant = getFindByQueryVariant(fullQueryMethod); + + reportInvalidUsage(node, { + queryMethod, + queryVariant, + prevQuery: fullQueryMethod, + fix(fixer) { + const { parent: expressionStatement } = node.parent; + const bodyText = sourceCode + .getText(argument.body) + .slice(1, -1) + .trim(); + const { line, column } = expressionStatement.loc.start; + const indent = sourceCode.getLines()[line - 1].slice(0, column); + const newText = bodyText + .split('\n') + .map((line) => line.trim()) + .join(`\n${indent}`); + return fixer.replaceText(expressionStatement, newText); + }, + }); + return; + } + + if (!isCallExpression(argument.body)) { + return; + } + + // ensure here it's one of the sync methods that we are calling + if (isScreenSyncQuery(argument)) { + const caller = getCaller(argument); + + if (!caller) { + return; + } + + // shape of () => screen.getByText + const fullQueryMethodNode = getWrongQueryName(argument); + + if (!fullQueryMethodNode) { + return; + } + + const fullQueryMethod = fullQueryMethodNode.name; + + // if there is a second argument to AwaitExpression, it is the options + const waitOptions = node.arguments[1]; + let waitOptionsSourceCode = ''; + if (isObjectExpression(waitOptions)) { + waitOptionsSourceCode = `, ${sourceCode.getText(waitOptions)}`; + } + + const queryVariant = getFindByQueryVariant(fullQueryMethod); + const callArguments = getQueryArguments(argument.body); + const queryMethod = fullQueryMethod.split('By')[1]; + + if (!queryMethod) { + return; + } + + reportInvalidUsage(node, { + queryMethod, + queryVariant, + prevQuery: fullQueryMethod, + fix(fixer) { + const property = ( + (argument.body as TSESTree.CallExpression) + .callee as TSESTree.MemberExpression + ).property; + if (helpers.isCustomQuery(property as TSESTree.Identifier)) { + return null; + } + const newCode = `${caller}.${queryVariant}${queryMethod}(${callArguments + .map((callArgNode) => sourceCode.getText(callArgNode)) + .join(', ')}${waitOptionsSourceCode})`; + return fixer.replaceText(node, newCode); + }, + }); + return; + } + + if (!isSyncQuery(argument)) { + return; + } + + // shape of () => getByText + const fullQueryMethodNode = getWrongQueryName(argument); + + if (!fullQueryMethodNode) { + return; + } + + const fullQueryMethod = fullQueryMethodNode.name; + + const queryMethod = fullQueryMethod.split('By')[1]; + const queryVariant = getFindByQueryVariant(fullQueryMethod); + const callArguments = getQueryArguments(argument.body); + + reportInvalidUsage(node, { + queryMethod, + queryVariant, + prevQuery: fullQueryMethod, + fix(fixer) { + // we know from above callee is an Identifier + if ( + helpers.isCustomQuery( + (argument.body as TSESTree.CallExpression) + .callee as TSESTree.Identifier + ) + ) { + return null; + } + const findByMethod = `${queryVariant}${queryMethod}`; + const allFixes: TSESLint.RuleFix[] = []; + // this updates waitFor with findBy* + const newCode = `${findByMethod}(${callArguments + .map((callArgNode) => sourceCode.getText(callArgNode)) + .join(', ')})`; + allFixes.push(fixer.replaceText(node, newCode)); + + // this adds the findBy* declaration - adding it to the list of destructured variables { findBy* } = render() + const definition = findRenderDefinitionDeclaration( + getScope(context, fullQueryMethodNode), + fullQueryMethod + ); + // I think it should always find it, otherwise code should not be valid (it'd be using undeclared variables) + if (!definition) { + return allFixes; + } + // check the declaration is part of a destructuring + if ( + definition.parent && + isObjectPattern(definition.parent.parent) + ) { + const allVariableDeclarations = definition.parent.parent; + // verify if the findBy* method was already declared + if ( + allVariableDeclarations.properties.some( + (p) => + isProperty(p) && + ASTUtils.isIdentifier(p.key) && + p.key.name === findByMethod + ) + ) { + return allFixes; + } + // the last character of a destructuring is always a "}", so we should replace it with the findBy* declaration + const textDestructuring = sourceCode.getText( + allVariableDeclarations + ); + const text = textDestructuring.replace( + /(\s*})$/, + `, ${findByMethod}$1` + ); + allFixes.push(fixer.replaceText(allVariableDeclarations, text)); + } + + return allFixes; + }, + }); + }, + }; + }, }); diff --git a/lib/rules/prefer-implicit-assert.ts b/lib/rules/prefer-implicit-assert.ts new file mode 100644 index 00000000..c1c404a0 --- /dev/null +++ b/lib/rules/prefer-implicit-assert.ts @@ -0,0 +1,137 @@ +import { + TSESTree, + ASTUtils, + AST_NODE_TYPES, + TSESLint, +} from '@typescript-eslint/utils'; + +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { TestingLibrarySettings } from '../create-testing-library-rule/detect-testing-library-utils'; +import { isCallExpression, isMemberExpression } from '../node-utils'; + +export const RULE_NAME = 'prefer-implicit-assert'; +export type MessageIds = 'preferImplicitAssert'; +type Options = []; + +const isCalledUsingSomeObject = (node: TSESTree.Identifier) => + isMemberExpression(node.parent) && + node.parent.object.type === AST_NODE_TYPES.Identifier; + +const isCalledInExpect = ( + node: TSESTree.Identifier | TSESTree.Node, + isAsyncQuery: boolean +) => { + if (isAsyncQuery) { + return ( + isCallExpression(node.parent) && + ASTUtils.isAwaitExpression(node.parent.parent) && + isCallExpression(node.parent.parent.parent) && + ASTUtils.isIdentifier(node.parent.parent.parent.callee) && + node.parent.parent.parent.callee.name === 'expect' + ); + } + return ( + isCallExpression(node.parent) && + isCallExpression(node.parent.parent) && + ASTUtils.isIdentifier(node.parent.parent.callee) && + node.parent.parent.callee.name === 'expect' + ); +}; + +const reportError = ( + context: Readonly< + TSESLint.RuleContext<'preferImplicitAssert', []> & { + settings: TestingLibrarySettings; + } + >, + node: TSESTree.Identifier | TSESTree.Node | undefined, + queryType: string +) => { + if (node) { + return context.report({ + node, + messageId: 'preferImplicitAssert', + data: { + queryType, + }, + }); + } +}; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: + 'Suggest using implicit assertions for getBy* & findBy* queries', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + svelte: false, + marko: false, + }, + }, + messages: { + preferImplicitAssert: + "Don't wrap `{{queryType}}` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `{{queryType}}` queries fail implicitly when element is not found", + }, + schema: [], + }, + defaultOptions: [], + create(context, _, helpers) { + const findQueryCalls: TSESTree.Identifier[] = []; + const getQueryCalls: TSESTree.Identifier[] = []; + + return { + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if (helpers.isFindQueryVariant(node)) { + findQueryCalls.push(node); + } + if (helpers.isGetQueryVariant(node)) { + getQueryCalls.push(node); + } + }, + 'Program:exit'() { + findQueryCalls.forEach((queryCall) => { + const isAsyncQuery = true; + const node: TSESTree.Identifier | TSESTree.Node | undefined = + isCalledUsingSomeObject(queryCall) ? queryCall.parent : queryCall; + + if (node) { + if (isCalledInExpect(node, isAsyncQuery)) { + if ( + isMemberExpression(node.parent?.parent?.parent?.parent) && + node.parent?.parent?.parent?.parent.property.type === + AST_NODE_TYPES.Identifier && + helpers.isPresenceAssert(node.parent.parent.parent.parent) + ) { + return reportError(context, node, 'findBy*'); + } + } + } + }); + + getQueryCalls.forEach((queryCall) => { + const isAsyncQuery = false; + const node: TSESTree.Identifier | TSESTree.Node | undefined = + isCalledUsingSomeObject(queryCall) ? queryCall.parent : queryCall; + if (node) { + if (isCalledInExpect(node, isAsyncQuery)) { + if ( + isMemberExpression(node.parent?.parent?.parent) && + node.parent?.parent?.parent.property.type === + AST_NODE_TYPES.Identifier && + helpers.isPresenceAssert(node.parent.parent.parent) + ) { + return reportError(context, node, 'getBy*'); + } + } + } + }); + }, + }; + }, +}); diff --git a/lib/rules/prefer-presence-queries.ts b/lib/rules/prefer-presence-queries.ts index 611cf5ed..504633c3 100644 --- a/lib/rules/prefer-presence-queries.ts +++ b/lib/rules/prefer-presence-queries.ts @@ -1,66 +1,113 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { findClosestCallNode, isMemberExpression } from '../node-utils'; export const RULE_NAME = 'prefer-presence-queries'; export type MessageIds = 'wrongAbsenceQuery' | 'wrongPresenceQuery'; -type Options = []; +export type Options = [ + { + presence?: boolean; + absence?: boolean; + }, +]; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - docs: { - description: - 'Ensure appropriate `get*`/`query*` queries are used with their respective matchers', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - wrongPresenceQuery: - 'Use `getBy*` queries rather than `queryBy*` for checking element is present', - wrongAbsenceQuery: - 'Use `queryBy*` queries rather than `getBy*` for checking element is NOT present', - }, - schema: [], - type: 'suggestion', - }, - defaultOptions: [], + name: RULE_NAME, + meta: { + docs: { + description: + 'Ensure appropriate `get*`/`query*` queries are used with their respective matchers', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + wrongPresenceQuery: + 'Use `getBy*` queries rather than `queryBy*` for checking element is present', + wrongAbsenceQuery: + 'Use `queryBy*` queries rather than `getBy*` for checking element is NOT present', + }, + fixable: 'code', + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + presence: { + type: 'boolean', + }, + absence: { + type: 'boolean', + }, + }, + }, + ], + type: 'suggestion', + }, + defaultOptions: [ + { + presence: true, + absence: true, + }, + ], - create(context, _, helpers) { - return { - 'CallExpression Identifier'(node: TSESTree.Identifier) { - const expectCallNode = findClosestCallNode(node, 'expect'); + create(context, [{ absence = true, presence = true }], helpers) { + return { + 'CallExpression Identifier'(node: TSESTree.Identifier) { + const expectCallNode = findClosestCallNode(node, 'expect'); + const withinCallNode = findClosestCallNode(node, 'within'); - if (!expectCallNode || !isMemberExpression(expectCallNode.parent)) { - return; - } + if (!isMemberExpression(expectCallNode?.parent)) { + return; + } - // Sync queries (getBy and queryBy) are corresponding ones used - // to check presence or absence. If none found, stop the rule. - if (!helpers.isSyncQuery(node)) { - return; - } + // Sync queries (getBy and queryBy) are corresponding ones used + // to check presence or absence. If none found, stop the rule. + if (!helpers.isSyncQuery(node)) { + return; + } - const isPresenceQuery = helpers.isGetQueryVariant(node); - const expectStatement = expectCallNode.parent; - const isPresenceAssert = helpers.isPresenceAssert(expectStatement); - const isAbsenceAssert = helpers.isAbsenceAssert(expectStatement); + const isPresenceQuery = helpers.isGetQueryVariant(node); + const expectStatement = expectCallNode.parent; + const isPresenceAssert = helpers.isPresenceAssert(expectStatement); + const isAbsenceAssert = helpers.isAbsenceAssert(expectStatement); - if (!isPresenceAssert && !isAbsenceAssert) { - return; - } + if (!isPresenceAssert && !isAbsenceAssert) { + return; + } - if (isPresenceAssert && !isPresenceQuery) { - context.report({ node, messageId: 'wrongPresenceQuery' }); - } else if (isAbsenceAssert && isPresenceQuery) { - context.report({ node, messageId: 'wrongAbsenceQuery' }); - } - }, - }; - }, + if ( + presence && + (withinCallNode || isPresenceAssert) && + !isPresenceQuery + ) { + const newQueryName = node.name.replace(/^query/, 'get'); + + context.report({ + node, + messageId: 'wrongPresenceQuery', + fix: (fixer) => fixer.replaceText(node, newQueryName), + }); + } else if ( + !withinCallNode && + absence && + isAbsenceAssert && + isPresenceQuery + ) { + const newQueryName = node.name.replace(/^get/, 'query'); + context.report({ + node, + messageId: 'wrongAbsenceQuery', + fix: (fixer) => fixer.replaceText(node, newQueryName), + }); + } + }, + }; + }, }); diff --git a/lib/rules/prefer-query-by-disappearance.ts b/lib/rules/prefer-query-by-disappearance.ts index bbb2a9e6..d09b6f8d 100644 --- a/lib/rules/prefer-query-by-disappearance.ts +++ b/lib/rules/prefer-query-by-disappearance.ts @@ -1,15 +1,15 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - getPropertyIdentifierNode, - isArrowFunctionExpression, - isCallExpression, - isMemberExpression, - isFunctionExpression, - isExpressionStatement, - isReturnStatement, - isBlockStatement, + getPropertyIdentifierNode, + isArrowFunctionExpression, + isCallExpression, + isMemberExpression, + isFunctionExpression, + isExpressionStatement, + isReturnStatement, + isBlockStatement, } from '../node-utils'; export const RULE_NAME = 'prefer-query-by-disappearance'; @@ -17,166 +17,174 @@ type MessageIds = 'preferQueryByDisappearance'; type Options = []; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: - 'Suggest using `queryBy*` queries when waiting for disappearance', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - preferQueryByDisappearance: - 'Prefer using queryBy* when waiting for disappearance', - }, - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - function isWaitForElementToBeRemoved(node: TSESTree.CallExpression) { - const identifierNode = getPropertyIdentifierNode(node); - - if (!identifierNode) { - return false; - } - - return helpers.isAsyncUtil(identifierNode, ['waitForElementToBeRemoved']); - } - - function isReportableExpression(node: TSESTree.LeftHandSideExpression) { - const argumentProperty = isMemberExpression(node) - ? getPropertyIdentifierNode(node.property) - : getPropertyIdentifierNode(node); - - if (!argumentProperty) { - return false; - } - - return ( - helpers.isGetQueryVariant(argumentProperty) || - helpers.isFindQueryVariant(argumentProperty) - ); - } - - function isNonCallbackViolation(node: TSESTree.CallExpressionArgument) { - if (!isCallExpression(node)) { - return false; - } - - if ( - !isMemberExpression(node.callee) && - !getPropertyIdentifierNode(node.callee) - ) { - return false; - } - - return isReportableExpression(node.callee); - } - - function isReturnViolation(node: TSESTree.Statement) { - if (!isReturnStatement(node) || !isCallExpression(node.argument)) { - return false; - } - - return isReportableExpression(node.argument.callee); - } - - function isNonReturnViolation(node: TSESTree.Statement) { - if (!isExpressionStatement(node) || !isCallExpression(node.expression)) { - return false; - } - - if ( - !isMemberExpression(node.expression.callee) && - !getPropertyIdentifierNode(node.expression.callee) - ) { - return false; - } - - return isReportableExpression(node.expression.callee); - } - - function isStatementViolation(statement: TSESTree.Statement) { - return isReturnViolation(statement) || isNonReturnViolation(statement); - } - - function isFunctionExpressionViolation( - node: TSESTree.CallExpressionArgument - ) { - if (!isFunctionExpression(node)) { - return false; - } - - return node.body.body.some((statement) => - isStatementViolation(statement) - ); - } - - function isArrowFunctionBodyViolation( - node: TSESTree.CallExpressionArgument - ) { - if (!isArrowFunctionExpression(node) || !isBlockStatement(node.body)) { - return false; - } - - return node.body.body.some((statement) => - isStatementViolation(statement) - ); - } - - function isArrowFunctionImplicitReturnViolation( - node: TSESTree.CallExpressionArgument - ) { - if (!isArrowFunctionExpression(node) || !isCallExpression(node.body)) { - return false; - } - - if ( - !isMemberExpression(node.body.callee) && - !getPropertyIdentifierNode(node.body.callee) - ) { - return false; - } - - return isReportableExpression(node.body.callee); - } - - function isArrowFunctionViolation(node: TSESTree.CallExpressionArgument) { - return ( - isArrowFunctionBodyViolation(node) || - isArrowFunctionImplicitReturnViolation(node) - ); - } - - function check(node: TSESTree.CallExpression) { - if (!isWaitForElementToBeRemoved(node)) { - return; - } - - const argumentNode = node.arguments[0]; - - if ( - !isNonCallbackViolation(argumentNode) && - !isArrowFunctionViolation(argumentNode) && - !isFunctionExpressionViolation(argumentNode) - ) { - return; - } - - context.report({ - node: argumentNode, - messageId: 'preferQueryByDisappearance', - }); - } - - return { - CallExpression: check, - }; - }, + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: + 'Suggest using `queryBy*` queries when waiting for disappearance', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + preferQueryByDisappearance: + 'Prefer using queryBy* when waiting for disappearance', + }, + schema: [], + }, + defaultOptions: [], + + create(context, _, helpers) { + function isWaitForElementToBeRemoved(node: TSESTree.CallExpression) { + const identifierNode = getPropertyIdentifierNode(node); + + if (!identifierNode) { + return false; + } + + return helpers.isAsyncUtil(identifierNode, ['waitForElementToBeRemoved']); + } + + /** + * Checks if node is reportable (starts with "get" or "find") and if it is, reports it with `context.report()`. + * + * @param {TSESTree.Expression} node - Node to be tested + * @returns {Boolean} Boolean indicating if expression was reported + */ + function reportExpression(node: TSESTree.Expression): boolean { + const argumentProperty = isMemberExpression(node) + ? getPropertyIdentifierNode(node.property) + : getPropertyIdentifierNode(node); + + if (!argumentProperty) { + return false; + } + + if ( + helpers.isGetQueryVariant(argumentProperty) || + helpers.isFindQueryVariant(argumentProperty) + ) { + context.report({ + node: argumentProperty, + messageId: 'preferQueryByDisappearance', + }); + return true; + } + return false; + } + + function checkNonCallbackViolation(node: TSESTree.CallExpressionArgument) { + if (!isCallExpression(node)) { + return false; + } + + if ( + !isMemberExpression(node.callee) && + !getPropertyIdentifierNode(node.callee) + ) { + return false; + } + + return reportExpression(node.callee); + } + + function isReturnViolation(node: TSESTree.Statement) { + if (!isReturnStatement(node) || !isCallExpression(node.argument)) { + return false; + } + + return reportExpression(node.argument.callee); + } + + function isNonReturnViolation(node: TSESTree.Statement) { + if (!isExpressionStatement(node) || !isCallExpression(node.expression)) { + return false; + } + + if ( + !isMemberExpression(node.expression.callee) && + !getPropertyIdentifierNode(node.expression.callee) + ) { + return false; + } + + return reportExpression(node.expression.callee); + } + + function isStatementViolation(statement: TSESTree.Statement) { + return isReturnViolation(statement) || isNonReturnViolation(statement); + } + + function checkFunctionExpressionViolation( + node: TSESTree.CallExpressionArgument + ) { + if (!isFunctionExpression(node)) { + return false; + } + + return node.body.body.some((statement) => + isStatementViolation(statement) + ); + } + + function isArrowFunctionBodyViolation( + node: TSESTree.CallExpressionArgument + ) { + if (!isArrowFunctionExpression(node) || !isBlockStatement(node.body)) { + return false; + } + + return node.body.body.some((statement) => + isStatementViolation(statement) + ); + } + + function isArrowFunctionImplicitReturnViolation( + node: TSESTree.CallExpressionArgument + ) { + if (!isArrowFunctionExpression(node) || !isCallExpression(node.body)) { + return false; + } + + if ( + !isMemberExpression(node.body.callee) && + !getPropertyIdentifierNode(node.body.callee) + ) { + return false; + } + + return reportExpression(node.body.callee); + } + + function checkArrowFunctionViolation( + node: TSESTree.CallExpressionArgument + ) { + return ( + isArrowFunctionBodyViolation(node) || + isArrowFunctionImplicitReturnViolation(node) + ); + } + + function check(node: TSESTree.CallExpression) { + if (!isWaitForElementToBeRemoved(node)) { + return; + } + + const argumentNode = node.arguments[0]; + + checkNonCallbackViolation(argumentNode); + checkArrowFunctionViolation(argumentNode); + checkFunctionExpressionViolation(argumentNode); + } + + return { + CallExpression: check, + }; + }, }); diff --git a/lib/rules/prefer-query-matchers.ts b/lib/rules/prefer-query-matchers.ts new file mode 100644 index 00000000..d11e49bb --- /dev/null +++ b/lib/rules/prefer-query-matchers.ts @@ -0,0 +1,107 @@ +import { TSESTree } from '@typescript-eslint/utils'; + +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { findClosestCallNode, isMemberExpression } from '../node-utils'; + +export const RULE_NAME = 'prefer-query-matchers'; +export type MessageIds = 'wrongQueryForMatcher'; +export type Options = [ + { + validEntries: { + query: 'get' | 'query'; + matcher: string; + }[]; + }, +]; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + docs: { + description: + 'Ensure the configured `get*`/`query*` query is used with the corresponding matchers', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + svelte: false, + marko: false, + }, + }, + messages: { + wrongQueryForMatcher: 'Use `{{ query }}By*` queries for {{ matcher }}', + }, + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + validEntries: { + type: 'array', + items: { + type: 'object', + properties: { + query: { + type: 'string', + enum: ['get', 'query'], + }, + matcher: { + type: 'string', + }, + }, + additionalProperties: false, + }, + }, + }, + }, + ], + type: 'suggestion', + }, + defaultOptions: [ + { + validEntries: [], + }, + ], + + create(context, [{ validEntries }], helpers) { + return { + 'CallExpression Identifier'(node: TSESTree.Identifier) { + const expectCallNode = findClosestCallNode(node, 'expect'); + + if (!expectCallNode || !isMemberExpression(expectCallNode.parent)) { + return; + } + + // Sync queries (getBy and queryBy) and corresponding ones + // are supported. If none found, stop the rule. + if (!helpers.isSyncQuery(node)) { + return; + } + + const isGetBy = helpers.isGetQueryVariant(node); + const expectStatement = expectCallNode.parent; + for (const entry of validEntries) { + const { query, matcher } = entry; + const isMatchingAssertForThisEntry = helpers.isMatchingAssert( + expectStatement, + matcher + ); + + if (!isMatchingAssertForThisEntry) { + continue; + } + + const actualQuery = isGetBy ? 'get' : 'query'; + if (query !== actualQuery) { + context.report({ + node, + messageId: 'wrongQueryForMatcher', + data: { query, matcher }, + }); + } + } + }, + }; + }, +}); diff --git a/lib/rules/prefer-screen-queries.ts b/lib/rules/prefer-screen-queries.ts index acca7c5a..4271e4b3 100644 --- a/lib/rules/prefer-screen-queries.ts +++ b/lib/rules/prefer-screen-queries.ts @@ -1,15 +1,15 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - getDeepestIdentifierNode, - getFunctionName, - getInnermostReturningFunction, - isCallExpression, - isMemberExpression, - isObjectExpression, - isObjectPattern, - isProperty, + getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, + isCallExpression, + isMemberExpression, + isObjectExpression, + isObjectPattern, + isProperty, } from '../node-utils'; export const RULE_NAME = 'prefer-screen-queries'; @@ -17,174 +17,176 @@ export type MessageIds = 'preferScreenQueries'; type Options = []; const ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING = [ - 'container', - 'baseElement', + 'container', + 'baseElement', ]; function usesContainerOrBaseElement(node: TSESTree.CallExpression) { - const secondArgument = node.arguments[1]; - return ( - isObjectExpression(secondArgument) && - secondArgument.properties.some( - (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING.includes(property.key.name) - ) - ); + const secondArgument = node.arguments[1]; + return ( + isObjectExpression(secondArgument) && + secondArgument.properties.some( + (property) => + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING.includes(property.key.name) + ) + ); } export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: 'Suggest using `screen` while querying', - recommendedConfig: { - dom: 'error', - angular: 'error', - react: 'error', - vue: 'error', - }, - }, - messages: { - preferScreenQueries: - 'Avoid destructuring queries from `render` result, use `screen.{{ name }}` instead', - }, - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - const renderWrapperNames: string[] = []; - - function detectRenderWrapper(node: TSESTree.Identifier): void { - const innerFunction = getInnermostReturningFunction(context, node); - - if (innerFunction) { - renderWrapperNames.push(getFunctionName(innerFunction)); - } - } - - function isReportableRender(node: TSESTree.Identifier): boolean { - return ( - helpers.isRenderUtil(node) || renderWrapperNames.includes(node.name) - ); - } - - function reportInvalidUsage(node: TSESTree.Identifier) { - context.report({ - node, - messageId: 'preferScreenQueries', - data: { - name: node.name, - }, - }); - } - - function saveSafeDestructuredQueries(node: TSESTree.VariableDeclarator) { - if (isObjectPattern(node.id)) { - for (const property of node.id.properties) { - if ( - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - helpers.isBuiltInQuery(property.key) - ) { - safeDestructuredQueries.push(property.key.name); - } - } - } - } - - function isIdentifierAllowed(name: string) { - return ['screen', ...withinDeclaredVariables].includes(name); - } - - // keep here those queries which are safe and shouldn't be reported - // (from within, from render + container/base element, not related to TL, etc) - const safeDestructuredQueries: string[] = []; - // use an array as within might be used more than once in a test - const withinDeclaredVariables: string[] = []; - - return { - VariableDeclarator(node) { - if ( - !isCallExpression(node.init) || - !ASTUtils.isIdentifier(node.init.callee) - ) { - return; - } - - const isComingFromValidRender = isReportableRender(node.init.callee); - - if (!isComingFromValidRender) { - // save the destructured query methods as safe since they are coming - // from render not related to TL - saveSafeDestructuredQueries(node); - } - - const isWithinFunction = node.init.callee.name === 'within'; - const usesRenderOptions = - isComingFromValidRender && usesContainerOrBaseElement(node.init); - - if (!isWithinFunction && !usesRenderOptions) { - return; - } - - if (isObjectPattern(node.id)) { - // save the destructured query methods as safe since they are coming - // from within or render + base/container options - saveSafeDestructuredQueries(node); - } else if (ASTUtils.isIdentifier(node.id)) { - withinDeclaredVariables.push(node.id.name); - } - }, - CallExpression(node) { - const identifierNode = getDeepestIdentifierNode(node); - - if (!identifierNode) { - return; - } - - if (helpers.isRenderUtil(identifierNode)) { - detectRenderWrapper(identifierNode); - } - - if (!helpers.isBuiltInQuery(identifierNode)) { - return; - } - - if (!isMemberExpression(identifierNode.parent)) { - const isSafeDestructuredQuery = safeDestructuredQueries.some( - (queryName) => queryName === identifierNode.name - ); - if (isSafeDestructuredQuery) { - return; - } - - reportInvalidUsage(identifierNode); - return; - } - - const memberExpressionNode = identifierNode.parent; - if ( - isCallExpression(memberExpressionNode.object) && - ASTUtils.isIdentifier(memberExpressionNode.object.callee) && - memberExpressionNode.object.callee.name !== 'within' && - isReportableRender(memberExpressionNode.object.callee) && - !usesContainerOrBaseElement(memberExpressionNode.object) - ) { - reportInvalidUsage(identifierNode); - return; - } - - if ( - ASTUtils.isIdentifier(memberExpressionNode.object) && - !isIdentifierAllowed(memberExpressionNode.object.name) - ) { - reportInvalidUsage(identifierNode); - } - }, - }; - }, + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: 'Suggest using `screen` while querying', + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + }, + messages: { + preferScreenQueries: + 'Avoid destructuring queries from `render` result, use `screen.{{ name }}` instead', + }, + schema: [], + }, + defaultOptions: [], + + create(context, _, helpers) { + const renderWrapperNames: string[] = []; + + function detectRenderWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + renderWrapperNames.push(getFunctionName(innerFunction)); + } + } + + function isReportableRender(node: TSESTree.Identifier): boolean { + return ( + helpers.isRenderUtil(node) || renderWrapperNames.includes(node.name) + ); + } + + function reportInvalidUsage(node: TSESTree.Identifier) { + context.report({ + node, + messageId: 'preferScreenQueries', + data: { + name: node.name, + }, + }); + } + + function saveSafeDestructuredQueries(node: TSESTree.VariableDeclarator) { + if (isObjectPattern(node.id)) { + for (const property of node.id.properties) { + if ( + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + helpers.isBuiltInQuery(property.key) + ) { + safeDestructuredQueries.push(property.key.name); + } + } + } + } + + function isIdentifierAllowed(name: string) { + return ['screen', ...withinDeclaredVariables].includes(name); + } + + // keep here those queries which are safe and shouldn't be reported + // (from within, from render + container/base element, not related to TL, etc) + const safeDestructuredQueries: string[] = []; + // use an array as within might be used more than once in a test + const withinDeclaredVariables: string[] = []; + + return { + VariableDeclarator(node) { + if ( + !isCallExpression(node.init) || + !ASTUtils.isIdentifier(node.init.callee) + ) { + return; + } + + const isComingFromValidRender = isReportableRender(node.init.callee); + + if (!isComingFromValidRender) { + // save the destructured query methods as safe since they are coming + // from render not related to TL + saveSafeDestructuredQueries(node); + } + + const isWithinFunction = node.init.callee.name === 'within'; + const usesRenderOptions = + isComingFromValidRender && usesContainerOrBaseElement(node.init); + + if (!isWithinFunction && !usesRenderOptions) { + return; + } + + if (isObjectPattern(node.id)) { + // save the destructured query methods as safe since they are coming + // from within or render + base/container options + saveSafeDestructuredQueries(node); + } else if (ASTUtils.isIdentifier(node.id)) { + withinDeclaredVariables.push(node.id.name); + } + }, + CallExpression(node) { + const identifierNode = getDeepestIdentifierNode(node); + + if (!identifierNode) { + return; + } + + if (helpers.isRenderUtil(identifierNode)) { + detectRenderWrapper(identifierNode); + } + + if (!helpers.isBuiltInQuery(identifierNode)) { + return; + } + + if (!isMemberExpression(identifierNode.parent)) { + const isSafeDestructuredQuery = safeDestructuredQueries.some( + (queryName) => queryName === identifierNode.name + ); + if (isSafeDestructuredQuery) { + return; + } + + reportInvalidUsage(identifierNode); + return; + } + + const memberExpressionNode = identifierNode.parent; + if ( + isCallExpression(memberExpressionNode.object) && + ASTUtils.isIdentifier(memberExpressionNode.object.callee) && + memberExpressionNode.object.callee.name !== 'within' && + isReportableRender(memberExpressionNode.object.callee) && + !usesContainerOrBaseElement(memberExpressionNode.object) + ) { + reportInvalidUsage(identifierNode); + return; + } + + if ( + ASTUtils.isIdentifier(memberExpressionNode.object) && + !isIdentifierAllowed(memberExpressionNode.object.name) + ) { + reportInvalidUsage(identifierNode); + } + }, + }; + }, }); diff --git a/lib/rules/prefer-user-event.ts b/lib/rules/prefer-user-event.ts index 2e0a8958..c278d1f4 100644 --- a/lib/rules/prefer-user-event.ts +++ b/lib/rules/prefer-user-event.ts @@ -1,10 +1,10 @@ -import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; +import { TSESTree, ASTUtils } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - findClosestCallExpressionNode, - isCallExpression, - isMemberExpression, + findClosestCallExpressionNode, + isCallExpression, + isMemberExpression, } from '../node-utils'; export const RULE_NAME = 'prefer-user-event'; @@ -13,182 +13,185 @@ export type MessageIds = 'preferUserEvent'; export type Options = [{ allowedMethods: string[] }]; export const UserEventMethods = [ - 'click', - 'dblClick', - 'type', - 'upload', - 'clear', - 'selectOptions', - 'deselectOptions', - 'tab', - 'hover', - 'unhover', - 'paste', + 'click', + 'dblClick', + 'type', + 'upload', + 'clear', + 'selectOptions', + 'deselectOptions', + 'tab', + 'hover', + 'unhover', + 'paste', ] as const; -type UserEventMethodsType = typeof UserEventMethods[number]; +type UserEventMethodsType = (typeof UserEventMethods)[number]; // maps fireEvent methods to userEvent. Those not found here, do not have an equivalent (yet) export const MAPPING_TO_USER_EVENT: Record = { - click: ['click', 'type', 'selectOptions', 'deselectOptions'], - change: ['upload', 'type', 'clear', 'selectOptions', 'deselectOptions'], - dblClick: ['dblClick'], - input: ['type', 'upload', 'selectOptions', 'deselectOptions', 'paste'], - keyDown: ['type', 'tab'], - keyPress: ['type'], - keyUp: ['type', 'tab'], - mouseDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], - mouseEnter: ['hover', 'selectOptions', 'deselectOptions'], - mouseLeave: ['unhover'], - mouseMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'], - mouseOut: ['unhover'], - mouseOver: ['hover', 'selectOptions', 'deselectOptions'], - mouseUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], - paste: ['paste'], - pointerDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], - pointerEnter: ['hover', 'selectOptions', 'deselectOptions'], - pointerLeave: ['unhover'], - pointerMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'], - pointerOut: ['unhover'], - pointerOver: ['hover', 'selectOptions', 'deselectOptions'], - pointerUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], + click: ['click', 'type', 'selectOptions', 'deselectOptions'], + change: ['upload', 'type', 'clear', 'selectOptions', 'deselectOptions'], + dblClick: ['dblClick'], + input: ['type', 'upload', 'selectOptions', 'deselectOptions', 'paste'], + keyDown: ['type', 'tab'], + keyPress: ['type'], + keyUp: ['type', 'tab'], + mouseDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], + mouseEnter: ['hover', 'selectOptions', 'deselectOptions'], + mouseLeave: ['unhover'], + mouseMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'], + mouseOut: ['unhover'], + mouseOver: ['hover', 'selectOptions', 'deselectOptions'], + mouseUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], + paste: ['paste'], + pointerDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], + pointerEnter: ['hover', 'selectOptions', 'deselectOptions'], + pointerLeave: ['unhover'], + pointerMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'], + pointerOut: ['unhover'], + pointerOver: ['hover', 'selectOptions', 'deselectOptions'], + pointerUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], }; function buildErrorMessage(fireEventMethod: string) { - const userEventMethods = MAPPING_TO_USER_EVENT[fireEventMethod].map( - (methodName) => `userEvent.${methodName}` - ); + const userEventMethods = MAPPING_TO_USER_EVENT[fireEventMethod].map( + (methodName) => `userEvent.${methodName}` + ); - // TODO: when min node version is 13, we can reimplement this using `Intl.ListFormat` - return userEventMethods.join(', ').replace(/, ([a-zA-Z.]+)$/, ', or $1'); + // TODO: when min node version is 13, we can reimplement this using `Intl.ListFormat` + return userEventMethods.join(', ').replace(/, ([a-zA-Z.]+)$/, ', or $1'); } const fireEventMappedMethods = Object.keys(MAPPING_TO_USER_EVENT); export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: - 'Suggest using `userEvent` over `fireEvent` for simulating user interactions', - recommendedConfig: { - dom: false, - angular: false, - react: false, - vue: false, - }, - }, - messages: { - preferUserEvent: - 'Prefer using {{userEventMethods}} over fireEvent.{{fireEventMethod}}', - }, - schema: [ - { - type: 'object', - properties: { - allowedMethods: { type: 'array' }, - }, - }, - ], - }, - defaultOptions: [{ allowedMethods: [] }], - - create(context, [options], helpers) { - const { allowedMethods } = options; - const createEventVariables: Record = {}; - - const isfireEventMethodAllowed = (methodName: string) => - !fireEventMappedMethods.includes(methodName) || - allowedMethods.includes(methodName); - - const getFireEventMethodName = ( - callExpressionNode: TSESTree.CallExpression, - node: TSESTree.Identifier - ) => { - if ( - !ASTUtils.isIdentifier(callExpressionNode.callee) && - !isMemberExpression(callExpressionNode.callee) - ) { - return node.name; - } - const secondArgument = callExpressionNode.arguments[1]; - if ( - ASTUtils.isIdentifier(secondArgument) && - createEventVariables[secondArgument.name] !== undefined - ) { - return createEventVariables[secondArgument.name]; - } - if ( - !isCallExpression(secondArgument) || - !helpers.isCreateEventUtil(secondArgument) - ) { - return node.name; - } - if (ASTUtils.isIdentifier(secondArgument.callee)) { - // createEvent('click', foo) - return (secondArgument.arguments[0] as TSESTree.Literal) - .value as string; - } - // createEvent.click(foo) - return ( - (secondArgument.callee as TSESTree.MemberExpression) - .property as TSESTree.Identifier - ).name; - }; - return { - 'CallExpression Identifier'(node: TSESTree.Identifier) { - if (!helpers.isFireEventMethod(node)) { - return; - } - const closestCallExpression = findClosestCallExpressionNode(node, true); - - if (!closestCallExpression) { - return; - } - - const fireEventMethodName = getFireEventMethodName( - closestCallExpression, - node - ); - - if ( - !fireEventMethodName || - isfireEventMethodAllowed(fireEventMethodName) - ) { - return; - } - context.report({ - node: closestCallExpression.callee, - messageId: 'preferUserEvent', - data: { - userEventMethods: buildErrorMessage(fireEventMethodName), - fireEventMethod: fireEventMethodName, - }, - }); - }, - - VariableDeclarator(node: TSESTree.VariableDeclarator) { - if ( - !isCallExpression(node.init) || - !helpers.isCreateEventUtil(node.init) || - !ASTUtils.isIdentifier(node.id) - ) { - return; - } - let fireEventMethodName = ''; - if ( - isMemberExpression(node.init.callee) && - ASTUtils.isIdentifier(node.init.callee.property) - ) { - fireEventMethodName = node.init.callee.property.name; - } else if (node.init.arguments.length > 0) { - fireEventMethodName = (node.init.arguments[0] as TSESTree.Literal) - .value as string; - } - if (!isfireEventMethodAllowed(fireEventMethodName)) { - createEventVariables[node.id.name] = fireEventMethodName; - } - }, - }; - }, + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: + 'Suggest using `userEvent` over `fireEvent` for simulating user interactions', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + svelte: false, + marko: false, + }, + }, + messages: { + preferUserEvent: + 'Prefer using {{userEventMethods}} over fireEvent.{{fireEventMethod}}', + }, + schema: [ + { + type: 'object', + properties: { + allowedMethods: { type: 'array' }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [{ allowedMethods: [] }], + + create(context, [options], helpers) { + const { allowedMethods } = options; + const createEventVariables: Record = {}; + + const isfireEventMethodAllowed = (methodName: string) => + !fireEventMappedMethods.includes(methodName) || + allowedMethods.includes(methodName); + + const getFireEventMethodName = ( + callExpressionNode: TSESTree.CallExpression, + node: TSESTree.Identifier + ) => { + if ( + !ASTUtils.isIdentifier(callExpressionNode.callee) && + !isMemberExpression(callExpressionNode.callee) + ) { + return node.name; + } + const secondArgument = callExpressionNode.arguments[1]; + if ( + ASTUtils.isIdentifier(secondArgument) && + createEventVariables[secondArgument.name] !== undefined + ) { + return createEventVariables[secondArgument.name]; + } + if ( + !isCallExpression(secondArgument) || + !helpers.isCreateEventUtil(secondArgument) + ) { + return node.name; + } + if (ASTUtils.isIdentifier(secondArgument.callee)) { + // createEvent('click', foo) + return (secondArgument.arguments[0] as TSESTree.Literal) + .value as string; + } + // createEvent.click(foo) + return ( + (secondArgument.callee as TSESTree.MemberExpression) + .property as TSESTree.Identifier + ).name; + }; + return { + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if (!helpers.isFireEventMethod(node)) { + return; + } + const closestCallExpression = findClosestCallExpressionNode(node, true); + + if (!closestCallExpression) { + return; + } + + const fireEventMethodName = getFireEventMethodName( + closestCallExpression, + node + ); + + if ( + !fireEventMethodName || + isfireEventMethodAllowed(fireEventMethodName) + ) { + return; + } + context.report({ + node: closestCallExpression.callee, + messageId: 'preferUserEvent', + data: { + userEventMethods: buildErrorMessage(fireEventMethodName), + fireEventMethod: fireEventMethodName, + }, + }); + }, + + VariableDeclarator(node: TSESTree.VariableDeclarator) { + if ( + !isCallExpression(node.init) || + !helpers.isCreateEventUtil(node.init) || + !ASTUtils.isIdentifier(node.id) + ) { + return; + } + let fireEventMethodName = ''; + if ( + isMemberExpression(node.init.callee) && + ASTUtils.isIdentifier(node.init.callee.property) + ) { + fireEventMethodName = node.init.callee.property.name; + } else if (node.init.arguments.length > 0) { + fireEventMethodName = (node.init.arguments[0] as TSESTree.Literal) + .value as string; + } + if (!isfireEventMethodAllowed(fireEventMethodName)) { + createEventVariables[node.id.name] = fireEventMethodName; + } + }, + }; + }, }); diff --git a/lib/rules/prefer-wait-for.ts b/lib/rules/prefer-wait-for.ts deleted file mode 100644 index a302ee1c..00000000 --- a/lib/rules/prefer-wait-for.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; - -import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { - isImportSpecifier, - isMemberExpression, - findClosestCallExpressionNode, - isCallExpression, - isImportNamespaceSpecifier, - isObjectPattern, - isProperty, -} from '../node-utils'; - -export const RULE_NAME = 'prefer-wait-for'; -export type MessageIds = - | 'preferWaitForImport' - | 'preferWaitForMethod' - | 'preferWaitForRequire'; -type Options = []; - -const DEPRECATED_METHODS = ['wait', 'waitForElement', 'waitForDomChange']; - -export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: 'Use `waitFor` instead of deprecated wait methods', - recommendedConfig: { - dom: false, - angular: false, - react: false, - vue: false, - }, - }, - messages: { - preferWaitForMethod: - '`{{ methodName }}` is deprecated in favour of `waitFor`', - preferWaitForImport: 'import `waitFor` instead of deprecated async utils', - preferWaitForRequire: - 'require `waitFor` instead of deprecated async utils', - }, - - fixable: 'code', - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - let addWaitFor = false; - - const reportRequire = (node: TSESTree.ObjectPattern) => { - context.report({ - node, - messageId: 'preferWaitForRequire', - fix(fixer) { - const excludedImports = [...DEPRECATED_METHODS, 'waitFor']; - - const newAllRequired = node.properties - .filter( - (s) => - isProperty(s) && - ASTUtils.isIdentifier(s.key) && - !excludedImports.includes(s.key.name) - ) - .map( - (s) => ((s as TSESTree.Property).key as TSESTree.Identifier).name - ); - - newAllRequired.push('waitFor'); - - return fixer.replaceText(node, `{ ${newAllRequired.join(',')} }`); - }, - }); - }; - - const reportImport = (node: TSESTree.ImportDeclaration) => { - context.report({ - node, - messageId: 'preferWaitForImport', - fix(fixer) { - const excludedImports = [...DEPRECATED_METHODS, 'waitFor']; - - // get all import names excluding all testing library `wait*` utils... - const newImports = node.specifiers - .map( - (specifier) => - isImportSpecifier(specifier) && - !excludedImports.includes(specifier.imported.name) && - specifier.imported.name - ) - .filter(Boolean) as string[]; - - // ... and append `waitFor` - newImports.push('waitFor'); - - // build new node with new imports and previous source value - const newNode = `import { ${newImports.join(',')} } from '${ - node.source.value - }';`; - - return fixer.replaceText(node, newNode); - }, - }); - }; - - const reportWait = (node: TSESTree.Identifier | TSESTree.JSXIdentifier) => { - context.report({ - node, - messageId: 'preferWaitForMethod', - data: { - methodName: node.name, - }, - fix(fixer) { - const callExpressionNode = findClosestCallExpressionNode(node); - if (!callExpressionNode) { - return null; - } - const [arg] = callExpressionNode.arguments; - const fixers = []; - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (arg) { - // if method been fixed already had a callback - // then we just replace the method name. - fixers.push(fixer.replaceText(node, 'waitFor')); - - if (node.name === 'waitForDomChange') { - // if method been fixed is `waitForDomChange` - // then the arg received was options object so we need to insert - // empty callback before. - fixers.push(fixer.insertTextBefore(arg, '() => {}, ')); - } - } else { - // if wait method been fixed didn't have any callback - // then we replace the method name and include an empty callback. - let methodReplacement = 'waitFor(() => {})'; - - // if wait method used like `foo.wait()` then we need to keep the - // member expression to get `foo.waitFor(() => {})` - if ( - isMemberExpression(node.parent) && - ASTUtils.isIdentifier(node.parent.object) - ) { - methodReplacement = `${node.parent.object.name}.${methodReplacement}`; - } - const newText = methodReplacement; - - fixers.push(fixer.replaceText(callExpressionNode, newText)); - } - - return fixers; - }, - }); - }; - - return { - 'CallExpression > MemberExpression'(node: TSESTree.MemberExpression) { - const isDeprecatedMethod = - ASTUtils.isIdentifier(node.property) && - DEPRECATED_METHODS.includes(node.property.name); - if (!isDeprecatedMethod) { - // the method does not match a deprecated method - return; - } - if (!helpers.isNodeComingFromTestingLibrary(node)) { - // the method does not match from the imported elements from TL (even from custom) - return; - } - addWaitFor = true; - reportWait(node.property as TSESTree.Identifier); // compiler is not picking up correctly, it should have inferred it is an identifier - }, - 'CallExpression > Identifier'(node: TSESTree.Identifier) { - if (!DEPRECATED_METHODS.includes(node.name)) { - return; - } - - if (!helpers.isNodeComingFromTestingLibrary(node)) { - return; - } - addWaitFor = true; - reportWait(node); - }, - 'Program:exit'() { - if (!addWaitFor) { - return; - } - // now that all usages of deprecated methods were replaced, remove the extra imports - const testingLibraryNode = - helpers.getCustomModuleImportNode() ?? - helpers.getTestingLibraryImportNode(); - if (isCallExpression(testingLibraryNode)) { - const parent = - testingLibraryNode.parent as TSESTree.VariableDeclarator; - if (!isObjectPattern(parent.id)) { - // if there is no destructuring, there is nothing to replace - return; - } - reportRequire(parent.id); - } else if (testingLibraryNode) { - if ( - testingLibraryNode.specifiers.length === 1 && - isImportNamespaceSpecifier(testingLibraryNode.specifiers[0]) - ) { - // if we import everything, there is nothing to replace - return; - } - reportImport(testingLibraryNode); - } - }, - }; - }, -}); diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index 046ce4ed..f1c4101b 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -1,11 +1,11 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - getDeepestIdentifierNode, - getFunctionName, - getInnermostReturningFunction, - isObjectPattern, + getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, + isObjectPattern, } from '../node-utils'; export const RULE_NAME = 'render-result-naming-convention'; @@ -14,97 +14,99 @@ export type MessageIds = 'renderResultNamingConvention'; type Options = []; const ALLOWED_VAR_NAMES = ['view', 'utils']; -const ALLOWED_VAR_NAMES_TEXT = ALLOWED_VAR_NAMES.map( - (name) => `\`${name}\`` -).join(', '); +const ALLOWED_VAR_NAMES_TEXT = ALLOWED_VAR_NAMES.map((name) => `\`${name}\``) + .join(', ') + .replace(/, ([^,]*)$/, ', or $1'); export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: 'Enforce a valid naming for return value from `render`', - 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}`, - }, - schema: [], - }, - defaultOptions: [], - - create(context, _, helpers) { - const renderWrapperNames: string[] = []; - - function detectRenderWrapper(node: TSESTree.Identifier): void { - const innerFunction = getInnermostReturningFunction(context, node); - - if (innerFunction) { - renderWrapperNames.push(getFunctionName(innerFunction)); - } - } - - return { - CallExpression(node) { - const callExpressionIdentifier = getDeepestIdentifierNode(node); - - if (!callExpressionIdentifier) { - return; - } - - if (helpers.isRenderUtil(callExpressionIdentifier)) { - detectRenderWrapper(callExpressionIdentifier); - } - }, - VariableDeclarator(node) { - if (!node.init) { - return; - } - const initIdentifierNode = getDeepestIdentifierNode(node.init); - - if (!initIdentifierNode) { - return; - } - - if ( - !helpers.isRenderVariableDeclarator(node) && - !renderWrapperNames.includes(initIdentifierNode.name) - ) { - return; - } - - // check if destructuring return value from render - if (isObjectPattern(node.id)) { - return; - } - - const renderResultName = ASTUtils.isIdentifier(node.id) && node.id.name; - - if (!renderResultName) { - return; - } - - const isAllowedRenderResultName = - ALLOWED_VAR_NAMES.includes(renderResultName); - - // check if return value var name is allowed - if (isAllowedRenderResultName) { - return; - } - - context.report({ - node, - messageId: 'renderResultNamingConvention', - data: { - renderResultName, - }, - }); - }, - }; - }, + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: 'Enforce a valid naming for return value from `render`', + recommendedConfig: { + dom: false, + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: '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}`, + }, + schema: [], + }, + defaultOptions: [], + + create(context, _, helpers) { + const renderWrapperNames: string[] = []; + + function detectRenderWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + renderWrapperNames.push(getFunctionName(innerFunction)); + } + } + + return { + CallExpression(node) { + const callExpressionIdentifier = getDeepestIdentifierNode(node); + + if (!callExpressionIdentifier) { + return; + } + + if (helpers.isRenderUtil(callExpressionIdentifier)) { + detectRenderWrapper(callExpressionIdentifier); + } + }, + VariableDeclarator(node) { + if (!node.init) { + return; + } + const initIdentifierNode = getDeepestIdentifierNode(node.init); + + if (!initIdentifierNode) { + return; + } + + if ( + !helpers.isRenderVariableDeclarator(node) && + !renderWrapperNames.includes(initIdentifierNode.name) + ) { + return; + } + + // check if destructuring return value from render + if (isObjectPattern(node.id)) { + return; + } + + const renderResultName = ASTUtils.isIdentifier(node.id) && node.id.name; + + if (!renderResultName) { + return; + } + + const isAllowedRenderResultName = + ALLOWED_VAR_NAMES.includes(renderResultName); + + // check if return value var name is allowed + if (isAllowedRenderResultName) { + return; + } + + context.report({ + node, + messageId: 'renderResultNamingConvention', + data: { + renderResultName, + }, + }); + }, + }; + }, }); diff --git a/lib/utils/compat.ts b/lib/utils/compat.ts new file mode 100644 index 00000000..0ad5e68c --- /dev/null +++ b/lib/utils/compat.ts @@ -0,0 +1,34 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + +/* istanbul ignore next */ +export const getFilename = ( + context: TSESLint.RuleContext +) => { + return context.filename ?? context.getFilename(); +}; + +/* istanbul ignore next */ +export const getSourceCode = ( + context: TSESLint.RuleContext +) => { + return context.sourceCode ?? context.getSourceCode(); +}; + +/* istanbul ignore next */ +export const getScope = ( + context: TSESLint.RuleContext, + node: TSESTree.Node +) => { + return getSourceCode(context).getScope?.(node) ?? context.getScope(); +}; + +/* istanbul ignore next */ +export const getDeclaredVariables = ( + context: TSESLint.RuleContext, + node: TSESTree.Node +) => { + return ( + getSourceCode(context).getDeclaredVariables?.(node) ?? + context.getDeclaredVariables(node) + ); +}; diff --git a/lib/utils/file-import.ts b/lib/utils/file-import.ts index e332ae29..c4198089 100644 --- a/lib/utils/file-import.ts +++ b/lib/utils/file-import.ts @@ -1,9 +1,9 @@ // 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 } => - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - obj?.__esModule ? obj : { default: obj }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-return + obj?.__esModule ? obj : { default: obj }; export const importDefault = (moduleName: string): T => - // eslint-disable-next-line @typescript-eslint/no-var-requires - interopRequireDefault(require(moduleName)).default; + // eslint-disable-next-line @typescript-eslint/no-require-imports + interopRequireDefault(require(moduleName)).default; diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 55997e11..faa97c51 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -1,140 +1,164 @@ +export * from './compat'; export * from './file-import'; export * from './types'; - -const combineQueries = (variants: string[], methods: string[]): string[] => { - const combinedQueries: string[] = []; - variants.forEach((variant) => { - const variantPrefix = variant.replace('By', ''); - methods.forEach((method) => { - combinedQueries.push(`${variantPrefix}${method}`); - }); - }); - - return combinedQueries; +export * from './resolve-to-testing-library-fn'; + +const combineQueries = ( + variants: readonly string[], + methods: readonly string[] +): string[] => { + const combinedQueries: string[] = []; + variants.forEach((variant) => { + const variantPrefix = variant.replace('By', ''); + methods.forEach((method) => { + combinedQueries.push(`${variantPrefix}${method}`); + }); + }); + + return combinedQueries; }; const getDocsUrl = (ruleName: string): string => - `https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/${ruleName}.md`; + `https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/${ruleName}.md`; const LIBRARY_MODULES = [ - '@testing-library/dom', - '@testing-library/angular', - '@testing-library/react', - '@testing-library/preact', - '@testing-library/vue', - '@testing-library/svelte', -]; - -const SYNC_QUERIES_VARIANTS = ['getBy', 'getAllBy', 'queryBy', 'queryAllBy']; -const ASYNC_QUERIES_VARIANTS = ['findBy', 'findAllBy']; + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/preact', + '@testing-library/vue', + '@testing-library/svelte', + '@marko/testing-library', +] as const; + +const USER_EVENT_MODULE = '@testing-library/user-event'; + +const OLD_LIBRARY_MODULES = [ + 'dom-testing-library', + 'vue-testing-library', + 'react-testing-library', +] as const; + +const SYNC_QUERIES_VARIANTS = [ + 'getBy', + 'getAllBy', + 'queryBy', + 'queryAllBy', +] as const; +const ASYNC_QUERIES_VARIANTS = ['findBy', 'findAllBy'] as const; const ALL_QUERIES_VARIANTS = [ - ...SYNC_QUERIES_VARIANTS, - ...ASYNC_QUERIES_VARIANTS, -]; + ...SYNC_QUERIES_VARIANTS, + ...ASYNC_QUERIES_VARIANTS, +] as const; const ALL_QUERIES_METHODS = [ - 'ByLabelText', - 'ByPlaceholderText', - 'ByText', - 'ByAltText', - 'ByTitle', - 'ByDisplayValue', - 'ByRole', - 'ByTestId', -]; + 'ByLabelText', + 'ByPlaceholderText', + 'ByText', + 'ByAltText', + 'ByTitle', + 'ByDisplayValue', + 'ByRole', + 'ByTestId', +] as const; const SYNC_QUERIES_COMBINATIONS = combineQueries( - SYNC_QUERIES_VARIANTS, - ALL_QUERIES_METHODS + SYNC_QUERIES_VARIANTS, + ALL_QUERIES_METHODS ); const ASYNC_QUERIES_COMBINATIONS = combineQueries( - ASYNC_QUERIES_VARIANTS, - ALL_QUERIES_METHODS + ASYNC_QUERIES_VARIANTS, + ALL_QUERIES_METHODS ); const ALL_QUERIES_COMBINATIONS = [ - ...SYNC_QUERIES_COMBINATIONS, - ...ASYNC_QUERIES_COMBINATIONS, -]; - -const ASYNC_UTILS = [ - 'waitFor', - 'waitForElementToBeRemoved', - 'wait', - 'waitForElement', - 'waitForDomChange', + ...SYNC_QUERIES_COMBINATIONS, + ...ASYNC_QUERIES_COMBINATIONS, ] as const; +const ASYNC_UTILS = ['waitFor', 'waitForElementToBeRemoved'] as const; + const DEBUG_UTILS = [ - 'debug', - 'logTestingPlaygroundURL', - 'prettyDOM', - 'logRoles', - 'logDOM', - 'prettyFormat', + 'debug', + 'logTestingPlaygroundURL', + 'prettyDOM', + 'logRoles', + 'logDOM', + 'prettyFormat', ] as const; const EVENTS_SIMULATORS = ['fireEvent', 'userEvent'] as const; -const TESTING_FRAMEWORK_SETUP_HOOKS = ['beforeEach', 'beforeAll']; +const TESTING_FRAMEWORK_SETUP_HOOKS = ['beforeEach', 'beforeAll'] as const; const PROPERTIES_RETURNING_NODES = [ - 'activeElement', - 'children', - 'firstChild', - 'firstElementChild', - 'fullscreenElement', - 'lastChild', - 'lastElementChild', - 'nextElementSibling', - 'nextSibling', - 'parentElement', - 'parentNode', - 'pointerLockElement', - 'previousElementSibling', - 'previousSibling', - 'rootNode', - 'scripts', -]; + 'activeElement', + 'children', + 'childElementCount', + 'firstChild', + 'firstElementChild', + 'fullscreenElement', + 'lastChild', + 'lastElementChild', + 'nextElementSibling', + 'nextSibling', + 'parentElement', + 'parentNode', + 'pointerLockElement', + 'previousElementSibling', + 'previousSibling', + 'rootNode', + 'scripts', +] as const; const METHODS_RETURNING_NODES = [ - 'closest', - 'getElementById', - 'getElementsByClassName', - 'getElementsByName', - 'getElementsByTagName', - 'getElementsByTagNameNS', - 'querySelector', - 'querySelectorAll', -]; + 'closest', + 'getElementById', + 'getElementsByClassName', + 'getElementsByName', + 'getElementsByTagName', + 'getElementsByTagNameNS', + 'querySelector', + 'querySelectorAll', +] as const; + +const EVENT_HANDLER_METHODS = ['click', 'select', 'submit'] as const; const ALL_RETURNING_NODES = [ - ...PROPERTIES_RETURNING_NODES, - ...METHODS_RETURNING_NODES, -]; + ...PROPERTIES_RETURNING_NODES, + ...METHODS_RETURNING_NODES, +] as const; -const PRESENCE_MATCHERS = ['toBeInTheDocument', 'toBeTruthy', 'toBeDefined']; -const ABSENCE_MATCHERS = ['toBeNull', 'toBeFalsy']; +const PRESENCE_MATCHERS = [ + 'toBeOnTheScreen', + 'toBeInTheDocument', + 'toBeTruthy', + 'toBeDefined', +] as const; +const ABSENCE_MATCHERS = ['toBeNull', 'toBeFalsy'] as const; export { - combineQueries, - getDocsUrl, - SYNC_QUERIES_VARIANTS, - ASYNC_QUERIES_VARIANTS, - ALL_QUERIES_VARIANTS, - ALL_QUERIES_METHODS, - SYNC_QUERIES_COMBINATIONS, - ASYNC_QUERIES_COMBINATIONS, - ALL_QUERIES_COMBINATIONS, - ASYNC_UTILS, - DEBUG_UTILS, - EVENTS_SIMULATORS, - TESTING_FRAMEWORK_SETUP_HOOKS, - LIBRARY_MODULES, - PROPERTIES_RETURNING_NODES, - METHODS_RETURNING_NODES, - ALL_RETURNING_NODES, - PRESENCE_MATCHERS, - ABSENCE_MATCHERS, + combineQueries, + getDocsUrl, + SYNC_QUERIES_VARIANTS, + ASYNC_QUERIES_VARIANTS, + ALL_QUERIES_VARIANTS, + ALL_QUERIES_METHODS, + SYNC_QUERIES_COMBINATIONS, + ASYNC_QUERIES_COMBINATIONS, + ALL_QUERIES_COMBINATIONS, + ASYNC_UTILS, + DEBUG_UTILS, + EVENTS_SIMULATORS, + TESTING_FRAMEWORK_SETUP_HOOKS, + LIBRARY_MODULES, + PROPERTIES_RETURNING_NODES, + METHODS_RETURNING_NODES, + ALL_RETURNING_NODES, + PRESENCE_MATCHERS, + ABSENCE_MATCHERS, + EVENT_HANDLER_METHODS, + USER_EVENT_MODULE, + OLD_LIBRARY_MODULES, }; diff --git a/lib/utils/is-testing-library-module.ts b/lib/utils/is-testing-library-module.ts new file mode 100644 index 00000000..548773a5 --- /dev/null +++ b/lib/utils/is-testing-library-module.ts @@ -0,0 +1,22 @@ +import { TestingLibrarySettings } from '../create-testing-library-rule/detect-testing-library-utils'; + +import { LIBRARY_MODULES, OLD_LIBRARY_MODULES, USER_EVENT_MODULE } from '.'; + +export const isOfficialTestingLibraryModule = (importSourceName: string) => + [...OLD_LIBRARY_MODULES, ...LIBRARY_MODULES, USER_EVENT_MODULE].includes( + importSourceName + ); + +export const isCustomTestingLibraryModule = ( + importSourceName: string, + customModuleSetting: TestingLibrarySettings['testing-library/utils-module'] +) => + typeof customModuleSetting === 'string' && + importSourceName.endsWith(customModuleSetting); + +export const isTestingLibraryModule = ( + importSourceName: string, + customModuleSetting?: TestingLibrarySettings['testing-library/utils-module'] +) => + isOfficialTestingLibraryModule(importSourceName) || + isCustomTestingLibraryModule(importSourceName, customModuleSetting); diff --git a/lib/utils/resolve-to-testing-library-fn.ts b/lib/utils/resolve-to-testing-library-fn.ts new file mode 100644 index 00000000..6d575cc3 --- /dev/null +++ b/lib/utils/resolve-to-testing-library-fn.ts @@ -0,0 +1,183 @@ +import { DefinitionType } from '@typescript-eslint/scope-manager'; +import { + AST_NODE_TYPES, + ASTUtils, + TSESLint, + TSESTree, +} from '@typescript-eslint/utils'; + +import { TestingLibraryContext } from '../create-testing-library-rule/detect-testing-library-utils'; +import { + isImportDefaultSpecifier, + isImportExpression, + isProperty, + isImportSpecifier, + isTSImportEqualsDeclaration, + isCallExpression, +} from '../node-utils'; +import { + AccessorNode, + getAccessorValue, + getStringValue, + isIdentifier, + isStringNode, + isSupportedAccessor, +} from '../node-utils/accessors'; + +import { isTestingLibraryModule } from './is-testing-library-module'; + +interface ImportDetails { + source: string; + local: string; + imported: string | null; +} + +const describeImportDefAsImport = ( + def: TSESLint.Scope.Definitions.ImportBindingDefinition +): ImportDetails | null => { + if (isTSImportEqualsDeclaration(def.parent)) { + return null; + } + + if (isImportDefaultSpecifier(def.node)) { + return { + source: def.parent.source.value, + imported: null, + local: def.node.local.name, + }; + } + + if (!isImportSpecifier(def.node)) { + return null; + } + + // we only care about value imports + if (def.parent.importKind === 'type') { + return null; + } + + return { + source: def.parent.source.value, + imported: + 'name' in def.node.imported + ? def.node.imported.name + : def.node.imported.value, + local: def.node.local.name, + }; +}; + +const describeVariableDefAsImport = ( + def: TSESLint.Scope.Definitions.VariableDefinition +): ImportDetails | null => { + if (!def.node.init) return null; + + const sourceNode = + isCallExpression(def.node.init) && + isIdentifier(def.node.init.callee, 'require') + ? def.node.init.arguments[0] + : ASTUtils.isAwaitExpression(def.node.init) && + isImportExpression(def.node.init.argument) + ? def.node.init.argument.source + : null; + + if (!sourceNode || !isStringNode(sourceNode)) return null; + if (!isProperty(def.name.parent)) return null; + if (!isSupportedAccessor(def.name.parent.key)) return null; + + return { + source: getStringValue(sourceNode), + imported: getAccessorValue(def.name.parent.key), + local: def.name.name, + }; +}; + +const describePossibleImportDef = ( + def: TSESLint.Scope.Definition +): ImportDetails | null => { + if (def.type === DefinitionType.Variable) { + return describeVariableDefAsImport(def); + } + if (def.type === DefinitionType.ImportBinding) { + return describeImportDefAsImport(def); + } + return null; +}; + +const resolveScope = ( + scope: TSESLint.Scope.Scope, + identifier: string +): ImportDetails | 'local' | null => { + let currentScope: TSESLint.Scope.Scope | null = scope; + while (currentScope !== null) { + const ref = currentScope.set.get(identifier); + if (ref && ref.defs.length > 0) { + const def = ref.defs[ref.defs.length - 1]; + const importDetails = describePossibleImportDef(def); + + if (importDetails?.local === identifier) { + return importDetails; + } + + return 'local'; + } + + currentScope = currentScope.upper; + } + + return null; +}; + +const joinChains = ( + a: AccessorNode[] | null, + b: AccessorNode[] | null +): AccessorNode[] | null => (a && b ? [...a, ...b] : null); + +export const getNodeChain = (node: TSESTree.Node): AccessorNode[] | null => { + if (isSupportedAccessor(node)) { + return [node]; + } + + switch (node.type) { + case AST_NODE_TYPES.MemberExpression: + return joinChains(getNodeChain(node.object), getNodeChain(node.property)); + case AST_NODE_TYPES.CallExpression: + return getNodeChain(node.callee); + } + + return null; +}; + +interface ResolvedTestingLibraryUserEventFn { + original: string | null; + local: string; +} + +export const resolveToTestingLibraryFn = < + TMessageIds extends string, + TOptions extends readonly unknown[], +>( + node: TSESTree.CallExpression, + context: TestingLibraryContext +): ResolvedTestingLibraryUserEventFn | null => { + const chain = getNodeChain(node); + if (!chain?.length) return null; + + const identifier = chain[0]; + const scope = context.sourceCode.getScope(identifier); + const maybeImport = resolveScope(scope, getAccessorValue(identifier)); + + if (maybeImport === 'local' || maybeImport === null) { + return null; + } + + const customModuleSetting = context.settings['testing-library/utils-module']; + + if (isTestingLibraryModule(maybeImport.source, customModuleSetting)) { + return { + original: maybeImport.imported, + local: maybeImport.local, + }; + } + + return null; +}; diff --git a/lib/utils/types.ts b/lib/utils/types.ts index 694d1d96..7f96e80d 100644 --- a/lib/utils/types.ts +++ b/lib/utils/types.ts @@ -1,35 +1,38 @@ -import type { TSESLint } from '@typescript-eslint/experimental-utils'; +import { TSESLint } from '@typescript-eslint/utils'; +type Recommended = 'error' | 'warn' | false; type RecommendedConfig = - | TSESLint.RuleMetaDataDocs['recommended'] - | [TSESLint.RuleMetaDataDocs['recommended'], ...TOptions]; + | Recommended + | [Recommended, ...TOptions]; -// These 2 types are copied from @typescript-eslint/experimental-utils' CreateRuleMeta -// and modified to our needs -export type TestingLibraryRuleMetaDocs = - Omit & { - /** - * 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[] -> = Omit, 'docs'> & { - docs: TestingLibraryRuleMetaDocs; +export type TestingLibraryPluginDocs = { + /** + * 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 TestingLibraryPluginRuleModule< + TMessageIds extends string, + TOptions extends readonly unknown[], +> = TSESLint.RuleModuleWithMetaDocs< + TMessageIds, + TOptions, + TestingLibraryPluginDocs +>; + export const SUPPORTED_TESTING_FRAMEWORKS = [ - 'dom', - 'angular', - 'react', - 'vue', + 'dom', + 'angular', + 'react', + 'vue', + 'svelte', + 'marko', ] as const; export type SupportedTestingFramework = - typeof SUPPORTED_TESTING_FRAMEWORKS[number]; + (typeof SUPPORTED_TESTING_FRAMEWORKS)[number]; diff --git a/lint-staged.config.js b/lint-staged.config.js new file mode 100644 index 00000000..cb757928 --- /dev/null +++ b/lint-staged.config.js @@ -0,0 +1,19 @@ +//eslint-disable-next-line @typescript-eslint/no-require-imports +const { ESLint } = require('eslint'); + +const removeIgnoredFiles = async (files) => { + const eslint = new ESLint(); + const ignoredFiles = await Promise.all( + files.map((file) => eslint.isPathIgnored(file)) + ); + const filteredFiles = files.filter((_, i) => !ignoredFiles[i]); + return filteredFiles.join(' '); +}; + +module.exports = { + '*.{js,ts}': async (files) => { + const filesToLint = await removeIgnoredFiles(files); + return [`eslint --max-warnings=0 ${filesToLint}`]; + }, + '*': 'prettier --write --ignore-unknown', +}; diff --git a/package.json b/package.json index 8d055a2b..576ecd80 100644 --- a/package.json +++ b/package.json @@ -1,83 +1,99 @@ { - "name": "eslint-plugin-testing-library", - "version": "0.0.0-semantically-released", - "description": "ESLint rules for Testing Library", - "keywords": [ - "eslint", - "eslintplugin", - "eslint-plugin", - "lint", - "testing-library", - "testing" - ], - "author": { - "name": "Mario BeltrΓ‘n AlarcΓ³n", - "email": "belco90@gmail.com", - "url": "https://mario.dev/" - }, - "repository": { - "type": "git", - "url": "https://github.com/testing-library/eslint-plugin-testing-library" - }, - "homepage": "https://github.com/testing-library/eslint-plugin-testing-library", - "bugs": { - "url": "https://github.com/testing-library/eslint-plugin-testing-library/issues" - }, - "main": "index.js", - "scripts": { - "build": "tsc", - "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", - "generate:rules-list": "ts-node tools/generate-rules-list", - "lint": "eslint . --max-warnings 0 --ext .js,.ts", - "lint:fix": "npm run lint -- --fix", - "test": "jest", - "test:ci": "jest --ci --coverage", - "test:update": "npm run test -- --u", - "test:watch": "npm run test -- --watch", - "type-check": "tsc --noEmit", - "semantic-release": "semantic-release", - "prepare": "is-ci || husky install" - }, - "dependencies": { - "@typescript-eslint/experimental-utils": "^5.0.0" - }, - "devDependencies": { - "@babel/eslint-plugin": "^7.14.5", - "@commitlint/cli": "^13.2.1", - "@commitlint/config-conventional": "^13.2.0", - "@types/jest": "^27.0.2", - "@types/node": "^16.10.6", - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.0.0", - "cpy-cli": "^3.1.1", - "eslint": "^7.32.0", - "eslint-config-kentcdodds": "^19.2.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jest": "^25.2.2", - "eslint-plugin-jest-formatting": "^3.0.0", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-promise": "^5.1.0", - "husky": "^7.0.2", - "is-ci": "^3.0.0", - "jest": "^27.3.0", - "lint-staged": "^11.2.3", - "prettier": "2.4.1", - "semantic-release": "^18.0.0", - "ts-jest": "27.0.7", - "ts-node": "^10.3.0", - "typescript": "^4.4.4" - }, - "peerDependencies": { - "eslint": "^7.5.0 || ^8.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6" - }, - "license": "MIT" + "name": "eslint-plugin-testing-library", + "version": "0.0.0-semantically-released", + "description": "ESLint plugin to follow best practices and anticipate common mistakes when writing tests with Testing Library", + "keywords": [ + "eslint", + "eslintplugin", + "eslint-plugin", + "lint", + "testing-library", + "testing" + ], + "homepage": "https://github.com/testing-library/eslint-plugin-testing-library", + "bugs": { + "url": "https://github.com/testing-library/eslint-plugin-testing-library/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/testing-library/eslint-plugin-testing-library" + }, + "license": "MIT", + "author": { + "name": "Mario BeltrΓ‘n AlarcΓ³n", + "email": "me@mario.dev", + "url": "https://mario.dev/" + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "index.d.ts" + ], + "main": "./dist/index.js", + "types": "index.d.ts", + "scripts": { + "prebuild": "del-cli dist", + "build": "tsc -p ./tsconfig.build.json", + "generate-all": "pnpm run --parallel \"/^generate:.*/\"", + "generate-all:check": "pnpm run generate-all && git diff --exit-code", + "generate:configs": "ts-node tools/generate-configs", + "generate:rules-doc": "pnpm run build && pnpm run rule-doc-generator", + "format": "pnpm run prettier-base --write", + "format:check": "pnpm run prettier-base --check", + "lint": "eslint . --max-warnings 0 --ext .js,.ts", + "lint:fix": "pnpm run lint --fix", + "prepare": "is-ci || husky", + "prettier-base": "prettier . --ignore-unknown --cache --log-level warn", + "rule-doc-generator": "eslint-doc-generator", + "semantic-release": "semantic-release", + "test": "jest", + "test:ci": "pnpm run test --ci --coverage", + "test:watch": "pnpm run test --watch", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@typescript-eslint/scope-manager": "^8.15.0", + "@typescript-eslint/utils": "^8.15.0" + }, + "devDependencies": { + "@commitlint/cli": "^19.6.0", + "@commitlint/config-conventional": "^19.6.0", + "@swc/core": "^1.9.3", + "@swc/jest": "^0.2.37", + "@types/jest": "^29.5.14", + "@types/node": "^22.9.3", + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", + "@typescript-eslint/rule-tester": "^8.15.0", + "del-cli": "^6.0.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.0", + "eslint-doc-generator": "^1.7.1", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jest": "^28.9.0", + "eslint-plugin-jest-formatting": "^3.1.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^7.1.0", + "eslint-remote-tester": "^3.0.1", + "eslint-remote-tester-repositories": "^1.0.1", + "husky": "^9.1.7", + "is-ci": "^3.0.1", + "jest": "^29.7.0", + "lint-staged": "^15.2.10", + "prettier": "3.6.2", + "semantic-release": "^24.2.0", + "semver": "^7.6.3", + "ts-node": "^10.9.2", + "typescript": "^5.7.2" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0", + "pnpm": "^9.14.0" + }, + "packageManager": "pnpm@9.14.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..2372bbba --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7722 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@typescript-eslint/scope-manager': + specifier: ^8.15.0 + version: 8.15.0 + '@typescript-eslint/utils': + specifier: ^8.15.0 + version: 8.15.0(eslint@8.57.1)(typescript@5.7.2) + devDependencies: + '@commitlint/cli': + specifier: ^19.6.0 + version: 19.8.0(@types/node@22.15.29)(typescript@5.7.2) + '@commitlint/config-conventional': + specifier: ^19.6.0 + version: 19.8.0 + '@swc/core': + specifier: ^1.9.3 + version: 1.9.3 + '@swc/jest': + specifier: ^0.2.37 + version: 0.2.37(@swc/core@1.9.3) + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/node': + specifier: ^22.9.3 + version: 22.15.29 + '@typescript-eslint/eslint-plugin': + specifier: ^8.15.0 + version: 8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) + '@typescript-eslint/parser': + specifier: ^8.15.0 + version: 8.15.0(eslint@8.57.1)(typescript@5.7.2) + '@typescript-eslint/rule-tester': + specifier: ^8.15.0 + version: 8.15.0(eslint@8.57.1)(typescript@5.7.2) + del-cli: + specifier: ^6.0.0 + version: 6.0.0 + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-doc-generator: + specifier: ^1.7.1 + version: 1.7.1(eslint@8.57.1)(typescript@5.7.2) + eslint-import-resolver-typescript: + specifier: ^3.6.3 + version: 3.6.3(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.31.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-plugin-jest: + specifier: ^28.9.0 + version: 28.12.0(@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)))(typescript@5.7.2) + eslint-plugin-jest-formatting: + specifier: ^3.1.0 + version: 3.1.0(eslint@8.57.1) + eslint-plugin-node: + specifier: ^11.1.0 + version: 11.1.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^7.1.0 + version: 7.2.1(eslint@8.57.1) + eslint-remote-tester: + specifier: ^3.0.1 + version: 3.0.1(eslint@8.57.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) + eslint-remote-tester-repositories: + specifier: ^1.0.1 + version: 1.0.1 + husky: + specifier: ^9.1.7 + version: 9.1.7 + is-ci: + specifier: ^3.0.1 + version: 3.0.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) + lint-staged: + specifier: ^15.2.10 + version: 15.4.3 + prettier: + specifier: 3.6.2 + version: 3.6.2 + semantic-release: + specifier: ^24.2.0 + version: 24.2.6(typescript@5.7.2) + semver: + specifier: ^7.6.3 + version: 7.7.1 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2) + typescript: + specifier: ^5.7.2 + version: 5.7.2 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.2': + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.2': + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.1': + resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.27.2': + resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.1': + resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@commitlint/cli@19.8.0': + resolution: {integrity: sha512-t/fCrLVu+Ru01h0DtlgHZXbHV2Y8gKocTR5elDOqIRUzQd0/6hpt2VIWOj9b3NDo7y4/gfxeR2zRtXq/qO6iUg==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@19.8.0': + resolution: {integrity: sha512-9I2kKJwcAPwMoAj38hwqFXG0CzS2Kj+SAByPUQ0SlHTfb7VUhYVmo7G2w2tBrqmOf7PFd6MpZ/a1GQJo8na8kw==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@19.8.0': + resolution: {integrity: sha512-+r5ZvD/0hQC3w5VOHJhGcCooiAVdynFlCe2d6I9dU+PvXdV3O+fU4vipVg+6hyLbQUuCH82mz3HnT/cBQTYYuA==} + engines: {node: '>=v18'} + + '@commitlint/ensure@19.8.0': + resolution: {integrity: sha512-kNiNU4/bhEQ/wutI1tp1pVW1mQ0QbAjfPRo5v8SaxoVV+ARhkB8Wjg3BSseNYECPzWWfg/WDqQGIfV1RaBFQZg==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@19.8.0': + resolution: {integrity: sha512-fuLeI+EZ9x2v/+TXKAjplBJWI9CNrHnyi5nvUQGQt4WRkww/d95oVRsc9ajpt4xFrFmqMZkd/xBQHZDvALIY7A==} + engines: {node: '>=v18'} + + '@commitlint/format@19.8.0': + resolution: {integrity: sha512-EOpA8IERpQstxwp/WGnDArA7S+wlZDeTeKi98WMOvaDLKbjptuHWdOYYr790iO7kTCif/z971PKPI2PkWMfOxg==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@19.8.0': + resolution: {integrity: sha512-L2Jv9yUg/I+jF3zikOV0rdiHUul9X3a/oU5HIXhAJLE2+TXTnEBfqYP9G5yMw/Yb40SnR764g4fyDK6WR2xtpw==} + engines: {node: '>=v18'} + + '@commitlint/lint@19.8.0': + resolution: {integrity: sha512-+/NZKyWKSf39FeNpqhfMebmaLa1P90i1Nrb1SrA7oSU5GNN/lksA4z6+ZTnsft01YfhRZSYMbgGsARXvkr/VLQ==} + engines: {node: '>=v18'} + + '@commitlint/load@19.8.0': + resolution: {integrity: sha512-4rvmm3ff81Sfb+mcWT5WKlyOa+Hd33WSbirTVUer0wjS1Hv/Hzr07Uv1ULIV9DkimZKNyOwXn593c+h8lsDQPQ==} + engines: {node: '>=v18'} + + '@commitlint/message@19.8.0': + resolution: {integrity: sha512-qs/5Vi9bYjf+ZV40bvdCyBn5DvbuelhR6qewLE8Bh476F7KnNyLfdM/ETJ4cp96WgeeHo6tesA2TMXS0sh5X4A==} + engines: {node: '>=v18'} + + '@commitlint/parse@19.8.0': + resolution: {integrity: sha512-YNIKAc4EXvNeAvyeEnzgvm1VyAe0/b3Wax7pjJSwXuhqIQ1/t2hD3OYRXb6D5/GffIvaX82RbjD+nWtMZCLL7Q==} + engines: {node: '>=v18'} + + '@commitlint/read@19.8.0': + resolution: {integrity: sha512-6ywxOGYajcxK1y1MfzrOnwsXO6nnErna88gRWEl3qqOOP8MDu/DTeRkGLXBFIZuRZ7mm5yyxU5BmeUvMpNte5w==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@19.8.0': + resolution: {integrity: sha512-CLanRQwuG2LPfFVvrkTrBR/L/DMy3+ETsgBqW1OvRxmzp/bbVJW0Xw23LnnExgYcsaFtos967lul1CsbsnJlzQ==} + engines: {node: '>=v18'} + + '@commitlint/rules@19.8.0': + resolution: {integrity: sha512-IZ5IE90h6DSWNuNK/cwjABLAKdy8tP8OgGVGbXe1noBEX5hSsu00uRlLu6JuruiXjWJz2dZc+YSw3H0UZyl/mA==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@19.8.0': + resolution: {integrity: sha512-3CKLUw41Cur8VMjh16y8LcsOaKbmQjAKCWlXx6B0vOUREplp6em9uIVhI8Cv934qiwkbi2+uv+mVZPnXJi1o9A==} + engines: {node: '>=v18'} + + '@commitlint/top-level@19.8.0': + resolution: {integrity: sha512-Rphgoc/omYZisoNkcfaBRPQr4myZEHhLPx2/vTXNLjiCw4RgfPR1wEgUpJ9OOmDCiv5ZyIExhprNLhteqH4FuQ==} + engines: {node: '>=v18'} + + '@commitlint/types@19.8.0': + resolution: {integrity: sha512-LRjP623jPyf3Poyfb0ohMj8I3ORyBDOwXAgxxVPbSD0unJuW2mJWeiRfaQinjtccMqC5Wy1HOMfa4btKjbNxbg==} + engines: {node: '>=v18'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/create-cache-key-function@29.7.0': + resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + + '@kwsites/promise-deferred@1.1.1': + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@octokit/auth-token@6.0.0': + resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} + engines: {node: '>= 20'} + + '@octokit/core@7.0.2': + resolution: {integrity: sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==} + engines: {node: '>= 20'} + + '@octokit/endpoint@11.0.0': + resolution: {integrity: sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==} + engines: {node: '>= 20'} + + '@octokit/graphql@9.0.1': + resolution: {integrity: sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==} + engines: {node: '>= 20'} + + '@octokit/openapi-types@25.1.0': + resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==} + + '@octokit/plugin-paginate-rest@13.1.1': + resolution: {integrity: sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-retry@8.0.1': + resolution: {integrity: sha512-KUoYR77BjF5O3zcwDQHRRZsUvJwepobeqiSSdCJ8lWt27FZExzb0GgVxrhhfuyF6z2B2zpO0hN5pteni1sqWiw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=7' + + '@octokit/plugin-throttling@11.0.1': + resolution: {integrity: sha512-S+EVhy52D/272L7up58dr3FNSMXWuNZolkL4zMJBNIfIxyZuUcczsQAU4b5w6dewJXnKYVgSHSV5wxitMSW1kw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': ^7.0.0 + + '@octokit/request-error@7.0.0': + resolution: {integrity: sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==} + engines: {node: '>= 20'} + + '@octokit/request@10.0.3': + resolution: {integrity: sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==} + engines: {node: '>= 20'} + + '@octokit/types@14.1.0': + resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@2.3.1': + resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} + engines: {node: '>=12'} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@semantic-release/commit-analyzer@13.0.1': + resolution: {integrity: sha512-wdnBPHKkr9HhNhXOhZD5a2LNl91+hs8CC2vsAVYxtZH3y0dV3wKn+uZSN61rdJQZ8EGxzWB3inWocBHV9+u/CQ==} + engines: {node: '>=20.8.1'} + peerDependencies: + semantic-release: '>=20.1.0' + + '@semantic-release/error@4.0.0': + resolution: {integrity: sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==} + engines: {node: '>=18'} + + '@semantic-release/github@11.0.3': + resolution: {integrity: sha512-T2fKUyFkHHkUNa5XNmcsEcDPuG23hwBKptfUVcFXDVG2cSjXXZYDOfVYwfouqbWo/8UefotLaoGfQeK+k3ep6A==} + engines: {node: '>=20.8.1'} + peerDependencies: + semantic-release: '>=24.1.0' + + '@semantic-release/npm@12.0.2': + resolution: {integrity: sha512-+M9/Lb35IgnlUO6OSJ40Ie+hUsZLuph2fqXC/qrKn0fMvUU/jiCjpoL6zEm69vzcmaZJ8yNKtMBEKHWN49WBbQ==} + engines: {node: '>=20.8.1'} + peerDependencies: + semantic-release: '>=20.1.0' + + '@semantic-release/release-notes-generator@14.0.3': + resolution: {integrity: sha512-XxAZRPWGwO5JwJtS83bRdoIhCiYIx8Vhr+u231pQAsdFIAbm19rSVJLdnBN+Avvk7CKvNQE/nJ4y7uqKH6WTiw==} + engines: {node: '>=20.8.1'} + peerDependencies: + semantic-release: '>=20.1.0' + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@swc/core-darwin-arm64@1.9.3': + resolution: {integrity: sha512-hGfl/KTic/QY4tB9DkTbNuxy5cV4IeejpPD4zo+Lzt4iLlDWIeANL4Fkg67FiVceNJboqg48CUX+APhDHO5G1w==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.9.3': + resolution: {integrity: sha512-IaRq05ZLdtgF5h9CzlcgaNHyg4VXuiStnOFpfNEMuI5fm5afP2S0FHq8WdakUz5WppsbddTdplL+vpeApt/WCQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.9.3': + resolution: {integrity: sha512-Pbwe7xYprj/nEnZrNBvZfjnTxlBIcfApAGdz2EROhjpPj+FBqBa3wOogqbsuGGBdCphf8S+KPprL1z+oDWkmSQ==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.9.3': + resolution: {integrity: sha512-AQ5JZiwNGVV/2K2TVulg0mw/3LYfqpjZO6jDPtR2evNbk9Yt57YsVzS+3vHSlUBQDRV9/jqMuZYVU3P13xrk+g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.9.3': + resolution: {integrity: sha512-tzVH480RY6RbMl/QRgh5HK3zn1ZTFsThuxDGo6Iuk1MdwIbdFYUY034heWUTI4u3Db97ArKh0hNL0xhO3+PZdg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.9.3': + resolution: {integrity: sha512-ivXXBRDXDc9k4cdv10R21ccBmGebVOwKXT/UdH1PhxUn9m/h8erAWjz5pcELwjiMf27WokqPgaWVfaclDbgE+w==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.9.3': + resolution: {integrity: sha512-ILsGMgfnOz1HwdDz+ZgEuomIwkP1PHT6maigZxaCIuC6OPEhKE8uYna22uU63XvYcLQvZYDzpR3ms47WQPuNEg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.9.3': + resolution: {integrity: sha512-e+XmltDVIHieUnNJHtspn6B+PCcFOMYXNJB1GqoCcyinkEIQNwC8KtWgMqUucUbEWJkPc35NHy9k8aCXRmw9Kg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.9.3': + resolution: {integrity: sha512-rqpzNfpAooSL4UfQnHhkW8aL+oyjqJniDP0qwZfGnjDoJSbtPysHg2LpcOBEdSnEH+uIZq6J96qf0ZFD8AGfXA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.9.3': + resolution: {integrity: sha512-3YJJLQ5suIEHEKc1GHtqVq475guiyqisKSoUnoaRtxkDaW5g1yvPt9IoSLOe2mRs7+FFhGGU693RsBUSwOXSdQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.9.3': + resolution: {integrity: sha512-oRj0AFePUhtatX+BscVhnzaAmWjpfAeySpM1TCbxA1rtBDeH/JDhi5yYzAKneDYtVtBvA7ApfeuzhMC9ye4xSg==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '*' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/jest@0.2.37': + resolution: {integrity: sha512-CR2BHhmXKGxTiFr21DYPRHQunLkX3mNIFGFkxBGji6r9uyIR5zftTOVYj1e0sFNMV2H7mf/+vpaglqaryBtqfQ==} + engines: {npm: '>= 7.0.0'} + peerDependencies: + '@swc/core': '*' + + '@swc/types@0.1.17': + resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/conventional-commits-parser@5.0.1': + resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@22.15.29': + resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@types/yoga-layout@1.9.2': + resolution: {integrity: sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==} + + '@typescript-eslint/eslint-plugin@8.15.0': + resolution: {integrity: sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.15.0': + resolution: {integrity: sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/rule-tester@8.15.0': + resolution: {integrity: sha512-G9lQX5jX64wrP5nI1nAEBj48dgyYFH8f0pjruQD9byK0Ln2cOyZPMt51rnzsm5ru8Nc7exV5SYyRppEhzaqSfg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/scope-manager@5.62.0': + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/scope-manager@8.15.0': + resolution: {integrity: sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.15.0': + resolution: {integrity: sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/types@8.15.0': + resolution: {integrity: sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@8.15.0': + resolution: {integrity: sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@5.62.0': + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@typescript-eslint/utils@8.15.0': + resolution: {integrity: sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/visitor-keys@8.15.0': + resolution: {integrity: sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + aggregate-error@5.0.0: + resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==} + engines: {node: '>=18'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + argv-formatter@1.0.0: + resolution: {integrity: sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==} + + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + auto-bind@4.0.0: + resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} + engines: {node: '>=8'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-preset-current-node-syntax@1.1.0: + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + before-after-hook@4.0.0: + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001684: + resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.1: + resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} + + clean-stack@5.2.0: + resolution: {integrity: sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==} + engines: {node: '>=14.16'} + + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + code-excerpt@3.0.0: + resolution: {integrity: sha512-VHNTVhd7KsLGOqfX3SyeO8RyYPMp1GJOg194VITk04WMYCv4plV68YWe6TJZxd9MhobjtpMRnVky01gqZsalaw==} + engines: {node: '>=10'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + + conventional-changelog-angular@8.0.0: + resolution: {integrity: sha512-CLf+zr6St0wIxos4bmaKHRXWAcsCXrJU6F4VdNDrGRK3B8LDLKoX3zuMV5GhtbGkVR/LohZ6MT6im43vZLSjmA==} + engines: {node: '>=18'} + + conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + + conventional-changelog-writer@8.1.0: + resolution: {integrity: sha512-dpC440QnORNCO81XYuRRFOLCsjKj4W7tMkUIn3lR6F/FAaJcWLi7iCj6IcEvSQY2zw6VUgwUKd5DEHKEWrpmEQ==} + engines: {node: '>=18'} + hasBin: true + + conventional-commits-filter@5.0.0: + resolution: {integrity: sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==} + engines: {node: '>=18'} + + conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + + conventional-commits-parser@6.2.0: + resolution: {integrity: sha512-uLnoLeIW4XaoFtH37qEcg/SXMJmKF4vi7V0H2rnPueg+VEtFGA/asSCNTcq4M/GQ6QmlzchAEtOoDTtKqWeHag==} + engines: {node: '>=18'} + hasBin: true + + convert-hrtime@5.0.0: + resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} + engines: {node: '>=12'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + convert-to-spaces@1.0.2: + resolution: {integrity: sha512-cj09EBuObp9gZNQCzc7hByQyrs6jVGE+o9kSJmeUoj+GiPiJvi5LYqEH/Hmme4+MTLHM+Ejtq+FChpjjEnsPdQ==} + engines: {node: '>= 4'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cosmiconfig-typescript-loader@6.1.0: + resolution: {integrity: sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + + dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + del-cli@6.0.0: + resolution: {integrity: sha512-9nitGV2W6KLFyya4qYt4+9AKQFL+c0Ehj5K7V7IwlxTc6RMCfQUGY9E9pLG6e8TQjtwXpuiWIGGZb3mfVxyZkw==} + engines: {node: '>=18'} + hasBin: true + + del@8.0.0: + resolution: {integrity: sha512-R6ep6JJ+eOBZsBr9esiNN1gxFbZE4Q2cULkUSFumGYecAiS6qodDvcPx/sFuWHMNul7DWmrtoEOpYSm7o6tbSA==} + engines: {node: '>=18'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dot-prop@7.2.0: + resolution: {integrity: sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + + electron-to-chromium@1.5.64: + resolution: {integrity: sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + + enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} + + env-ci@11.1.1: + resolution: {integrity: sha512-mT3ks8F0kwpo7SYNds6nWj0PaRh+qJxIeBVBXAKTN9hphAzZv7s0QAZQbqnB1fAv/r4pJUGE15BV9UrS31FP2w==} + engines: {node: ^18.17 || >=20.6.1} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-abstract@1.23.5: + resolution: {integrity: sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + + es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-doc-generator@1.7.1: + resolution: {integrity: sha512-i1Zjl+Xcy712SZhbceCeMVaIdhbFqY27i8d7f9gyb9P/6AQNnPA0VCWynAFVGYa0hpeR5kwUI09+GBELgC2nnA==} + engines: {node: ^14.18.0 || ^16.0.0 || >=18.0.0} + hasBin: true + peerDependencies: + eslint: '>= 7' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.6.3: + resolution: {integrity: sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.0: + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-es@3.0.1: + resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + + eslint-plugin-import@2.31.0: + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jest-formatting@3.1.0: + resolution: {integrity: sha512-XyysraZ1JSgGbLSDxjj5HzKKh0glgWf+7CkqxbTqb7zEhW7X2WHo5SBQ8cGhnszKN+2Lj3/oevBlHNbHezoc/A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=0.8.0' + + eslint-plugin-jest@28.12.0: + resolution: {integrity: sha512-J6zmDp8WiQ9tyvYXE+3RFy7/+l4hraWLzmsabYXyehkmmDd36qV4VQFc7XzcsD8C1PTNt646MSx25bO1mdd9Yw==} + engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^6.0.0 || ^7.0.0 || ^8.0.0 + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + jest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + jest: + optional: true + + eslint-plugin-node@11.1.0: + resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=5.16.0' + + eslint-plugin-promise@7.2.1: + resolution: {integrity: sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-remote-tester-repositories@1.0.1: + resolution: {integrity: sha512-XL9PqGBDL4rORnQ74b/3tqbJUpMlPz9gzKSrmYFtakLBQ/ayBELB/HZvd6ZEl+mH0vBeSYtUH7E/rawBsf9Qzg==} + + eslint-remote-tester@3.0.1: + resolution: {integrity: sha512-/jifRW0gJ5NmrWGD8mn2imvafO0fS6KBKLzv8ZIdI1uMHZ2EriYN7Fw4cyOR7rfbt6Ve2tUrluSvVptW1PxEvg==} + engines: {node: '>=12.11'} + hasBin: true + peerDependencies: + eslint: '>=7' + ts-node: '>=9.0.0' + peerDependenciesMeta: + ts-node: + optional: true + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + + eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + fast-content-type-parse@3.0.0: + resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + + find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + find-versions@6.0.0: + resolution: {integrity: sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==} + engines: {node: '>=18'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function-timeout@1.0.2: + resolution: {integrity: sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==} + engines: {node: '>=18'} + + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@7.0.1: + resolution: {integrity: sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==} + engines: {node: '>=16'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + + git-log-parser@1.2.1: + resolution: {integrity: sha512-PI+sPDvHXNPl5WNOErAK05s3j0lgwUzMN6o8cyQrDaKfT3qd7TmNJKeXX+SknI5I0QhG5fVPAEwSY4tRGDtYoQ==} + + git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@14.0.2: + resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} + engines: {node: '>=18'} + + globby@14.1.0: + resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} + engines: {node: '>=18'} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + hook-std@3.0.0: + resolution: {integrity: sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + + hosted-git-info@8.1.0: + resolution: {integrity: sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==} + engines: {node: ^18.17.0 || >=20.5.0} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-from-esm@2.0.0: + resolution: {integrity: sha512-YVt14UZCgsX1vZQ3gKjkWVdBdHQ6eu3MPU1TBgL1H5orXe2+jWD006WCPPtOuwlQm10NuzOW5WawiF1Q9veW8g==} + engines: {node: '>=18.20'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + index-to-position@1.1.0: + resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==} + engines: {node: '>=18'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ink@3.2.0: + resolution: {integrity: sha512-firNp1q3xxTzoItj/eOOSZQnYSlyrWks5llCTVX37nJ59K3eXbQ8PtzCguqo8YI19EELo5QxaKnJd4VxzhU8tg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '>=16.8.0' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + + into-stream@7.0.0: + resolution: {integrity: sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==} + engines: {node: '>=12'} + + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + + is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + + is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + + is-bun-module@1.2.1: + resolution: {integrity: sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-ci@2.0.0: + resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} + hasBin: true + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + + is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.0: + resolution: {integrity: sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-path-cwd@3.0.0: + resolution: {integrity: sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + + is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + + is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + + is-weakset@2.0.3: + resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} + engines: {node: '>= 0.4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + issue-parser@7.0.1: + resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==} + engines: {node: ^18.17 || >=20.6.1} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + java-properties@1.0.2: + resolution: {integrity: sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==} + engines: {node: '>= 0.6.0'} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lint-staged@15.4.3: + resolution: {integrity: sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.2.5: + resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} + engines: {node: '>=18.0.0'} + + load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + + locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.capitalize@4.2.1: + resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.uniqby@4.7.0: + resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + marked-terminal@7.3.0: + resolution: {integrity: sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==} + engines: {node: '>=16.0.0'} + peerDependencies: + marked: '>=1 <16' + + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime@4.0.7: + resolution: {integrity: sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ==} + engines: {node: '>=16'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + nerf-dart@1.0.0: + resolution: {integrity: sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + normalize-package-data@6.0.2: + resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} + engines: {node: ^16.14.0 || >=18.0.0} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-url@8.0.2: + resolution: {integrity: sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==} + engines: {node: '>=14.16'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + npm@10.9.3: + resolution: {integrity: sha512-6Eh1u5Q+kIVXeA8e7l2c/HpnFFcwrkt37xDMujD5be1gloWa9p6j3Fsv3mByXXmqJHy+2cElRMML8opNT7xIJQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + bundledDependencies: + - '@isaacs/string-locale-compare' + - '@npmcli/arborist' + - '@npmcli/config' + - '@npmcli/fs' + - '@npmcli/map-workspaces' + - '@npmcli/package-json' + - '@npmcli/promise-spawn' + - '@npmcli/redact' + - '@npmcli/run-script' + - '@sigstore/tuf' + - abbrev + - archy + - cacache + - chalk + - ci-info + - cli-columns + - fastest-levenshtein + - fs-minipass + - glob + - graceful-fs + - hosted-git-info + - ini + - init-package-json + - is-cidr + - json-parse-even-better-errors + - libnpmaccess + - libnpmdiff + - libnpmexec + - libnpmfund + - libnpmhook + - libnpmorg + - libnpmpack + - libnpmpublish + - libnpmsearch + - libnpmteam + - libnpmversion + - make-fetch-happen + - minimatch + - minipass + - minipass-pipeline + - ms + - node-gyp + - nopt + - normalize-package-data + - npm-audit-report + - npm-install-checks + - npm-package-arg + - npm-pick-manifest + - npm-profile + - npm-registry-fetch + - npm-user-validate + - p-map + - pacote + - parse-conflict-json + - proc-log + - qrcode-terminal + - read + - semver + - spdx-expression-parse + - ssri + - supports-color + - tar + - text-table + - tiny-relative-date + - treeverse + - validate-npm-package-name + - which + - write-file-atomic + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-each-series@3.0.0: + resolution: {integrity: sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==} + engines: {node: '>=12'} + + p-filter@4.1.0: + resolution: {integrity: sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==} + engines: {node: '>=18'} + + p-is-promise@3.0.0: + resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} + engines: {node: '>=8'} + + p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-map@7.0.2: + resolution: {integrity: sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==} + engines: {node: '>=18'} + + p-map@7.0.3: + resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==} + engines: {node: '>=18'} + + p-reduce@3.0.0: + resolution: {integrity: sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==} + engines: {node: '>=12'} + + p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + patch-console@1.0.0: + resolution: {integrity: sha512-nxl9nrnLQmh64iTzMfyylSlRozL7kAXIaxw1fVcLYdyhNkJCRUzirRZTikXGJsg+hc4fqpneTK6iU2H1Q8THSA==} + engines: {node: '>=10'} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + + path-type@6.0.0: + resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} + engines: {node: '>=18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pkg-conf@2.1.0: + resolution: {integrity: sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==} + engines: {node: '>=4'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + pretty-ms@9.2.0: + resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} + engines: {node: '>=18'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-devtools-core@4.28.5: + resolution: {integrity: sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-reconciler@0.26.2: + resolution: {integrity: sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^17.0.2 + + react@17.0.2: + resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} + engines: {node: '>=0.10.0'} + + read-package-up@11.0.0: + resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} + engines: {node: '>=18'} + + read-pkg@9.0.1: + resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} + engines: {node: '>=18'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + reflect.getprototypeof@1.0.7: + resolution: {integrity: sha512-bMvFGIUKlc/eSfXNX+aZ+EL95/EgZzuwA0OBPTbZZDEJw/0AkentjMuM1oiRfwHrshqk4RzdgiTg5CcDalXN5g==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.3: + resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} + engines: {node: '>= 0.4'} + + regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + + registry-auth-token@5.1.0: + resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==} + engines: {node: '>=14'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + + scheduler@0.20.2: + resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==} + + semantic-release@24.2.6: + resolution: {integrity: sha512-D0cwjlO5RZzHHxAcsoF1HxiRLfC3ehw+ay+zntzFs6PNX6aV0JzKNG15mpxPipBYa/l4fHly88dHvgDyqwb1Ww==} + engines: {node: '>=20.8.1'} + hasBin: true + + semver-diff@4.0.0: + resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} + engines: {node: '>=12'} + + semver-regex@4.0.5: + resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} + engines: {node: '>=12'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + signale@1.4.0: + resolution: {integrity: sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==} + engines: {node: '>=6'} + + simple-git@3.27.0: + resolution: {integrity: sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spawn-error-forwarder@1.0.0: + resolution: {integrity: sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.21: + resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + + split2@1.0.0: + resolution: {integrity: sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stream-combiner2@1.1.1: + resolution: {integrity: sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + super-regex@1.0.0: + resolution: {integrity: sha512-CY8u7DtbvucKuquCmOFEKhr9Besln7n9uN8eFbwcoGYWXOMW07u2o8njWaiXt11ylS3qoGF55pILjRmPlbodyg==} + engines: {node: '>=18'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + temp-dir@3.0.0: + resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} + engines: {node: '>=14.16'} + + tempy@3.1.0: + resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==} + engines: {node: '>=14.16'} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + time-span@5.1.0: + resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} + engines: {node: '>=12'} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + traverse@0.6.8: + resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==} + engines: {node: '>= 0.4'} + + ts-api-utils@1.4.1: + resolution: {integrity: sha512-5RU2/lxTA3YUZxju61HO2U6EoZLvBLtmV2mbTvqyu4a/7s7RmJPT+1YekhMVsQhznRWk/czIwDUg+V8Q9ZuG4w==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.12.0: + resolution: {integrity: sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==} + engines: {node: '>=10'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.3: + resolution: {integrity: sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-join@5.0.0: + resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + + which-builtin-type@1.2.0: + resolution: {integrity: sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + + yoga-layout-prebuilt@1.10.0: + resolution: {integrity: sha512-YnOmtSbv4MTf7RGJMK0FvZ+KD8OEe/J5BNnR0GHhD8J/XcG/Qvxgszm0Un6FTHWW4uHlTgP0IztiXQnGyIR45g==} + engines: {node: '>=8'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.2': {} + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.27.1 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.26.2': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.25.9': {} + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.27.1': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.1 + + '@babel/parser@7.26.2': + dependencies: + '@babel/types': 7.26.0 + + '@babel/parser@7.27.2': + dependencies: + '@babel/types': 7.27.1 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + + '@babel/traverse@7.25.9': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.4.1 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@babel/types@7.27.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@0.2.3': {} + + '@colors/colors@1.5.0': + optional: true + + '@commitlint/cli@19.8.0(@types/node@22.15.29)(typescript@5.7.2)': + dependencies: + '@commitlint/format': 19.8.0 + '@commitlint/lint': 19.8.0 + '@commitlint/load': 19.8.0(@types/node@22.15.29)(typescript@5.7.2) + '@commitlint/read': 19.8.0 + '@commitlint/types': 19.8.0 + tinyexec: 0.3.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/config-conventional@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + conventional-changelog-conventionalcommits: 7.0.2 + + '@commitlint/config-validator@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + ajv: 8.17.1 + + '@commitlint/ensure@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@19.8.0': {} + + '@commitlint/format@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + chalk: 5.4.1 + + '@commitlint/is-ignored@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + semver: 7.7.1 + + '@commitlint/lint@19.8.0': + dependencies: + '@commitlint/is-ignored': 19.8.0 + '@commitlint/parse': 19.8.0 + '@commitlint/rules': 19.8.0 + '@commitlint/types': 19.8.0 + + '@commitlint/load@19.8.0(@types/node@22.15.29)(typescript@5.7.2)': + dependencies: + '@commitlint/config-validator': 19.8.0 + '@commitlint/execute-rule': 19.8.0 + '@commitlint/resolve-extends': 19.8.0 + '@commitlint/types': 19.8.0 + chalk: 5.4.1 + cosmiconfig: 9.0.0(typescript@5.7.2) + cosmiconfig-typescript-loader: 6.1.0(@types/node@22.15.29)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@19.8.0': {} + + '@commitlint/parse@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + conventional-changelog-angular: 7.0.0 + conventional-commits-parser: 5.0.0 + + '@commitlint/read@19.8.0': + dependencies: + '@commitlint/top-level': 19.8.0 + '@commitlint/types': 19.8.0 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + tinyexec: 0.3.2 + + '@commitlint/resolve-extends@19.8.0': + dependencies: + '@commitlint/config-validator': 19.8.0 + '@commitlint/types': 19.8.0 + global-directory: 4.0.1 + import-meta-resolve: 4.1.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@19.8.0': + dependencies: + '@commitlint/ensure': 19.8.0 + '@commitlint/message': 19.8.0 + '@commitlint/to-lines': 19.8.0 + '@commitlint/types': 19.8.0 + + '@commitlint/to-lines@19.8.0': {} + + '@commitlint/top-level@19.8.0': + dependencies: + find-up: 7.0.0 + + '@commitlint/types@19.8.0': + dependencies: + '@types/conventional-commits-parser': 5.0.1 + chalk: 5.4.1 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@eslint-community/eslint-utils@4.4.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/create-cache-key-function@29.7.0': + dependencies: + '@jest/types': 29.6.3 + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.15.29 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + '@types/node': 22.15.29 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.26.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.15.29 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@kwsites/promise-deferred@1.1.1': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@octokit/auth-token@6.0.0': {} + + '@octokit/core@7.0.2': + dependencies: + '@octokit/auth-token': 6.0.0 + '@octokit/graphql': 9.0.1 + '@octokit/request': 10.0.3 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.1.0 + before-after-hook: 4.0.0 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@11.0.0': + dependencies: + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@9.0.1': + dependencies: + '@octokit/request': 10.0.3 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/openapi-types@25.1.0': {} + + '@octokit/plugin-paginate-rest@13.1.1(@octokit/core@7.0.2)': + dependencies: + '@octokit/core': 7.0.2 + '@octokit/types': 14.1.0 + + '@octokit/plugin-retry@8.0.1(@octokit/core@7.0.2)': + dependencies: + '@octokit/core': 7.0.2 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.1.0 + bottleneck: 2.19.5 + + '@octokit/plugin-throttling@11.0.1(@octokit/core@7.0.2)': + dependencies: + '@octokit/core': 7.0.2 + '@octokit/types': 14.1.0 + bottleneck: 2.19.5 + + '@octokit/request-error@7.0.0': + dependencies: + '@octokit/types': 14.1.0 + + '@octokit/request@10.0.3': + dependencies: + '@octokit/endpoint': 11.0.0 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.1.0 + fast-content-type-parse: 3.0.0 + universal-user-agent: 7.0.3 + + '@octokit/types@14.1.0': + dependencies: + '@octokit/openapi-types': 25.1.0 + + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@2.3.1': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + + '@rtsao/scc@1.1.0': {} + + '@sec-ant/readable-stream@0.4.1': {} + + '@semantic-release/commit-analyzer@13.0.1(semantic-release@24.2.6(typescript@5.7.2))': + dependencies: + conventional-changelog-angular: 8.0.0 + conventional-changelog-writer: 8.1.0 + conventional-commits-filter: 5.0.0 + conventional-commits-parser: 6.2.0 + debug: 4.4.1 + import-from-esm: 2.0.0 + lodash-es: 4.17.21 + micromatch: 4.0.8 + semantic-release: 24.2.6(typescript@5.7.2) + transitivePeerDependencies: + - supports-color + + '@semantic-release/error@4.0.0': {} + + '@semantic-release/github@11.0.3(semantic-release@24.2.6(typescript@5.7.2))': + dependencies: + '@octokit/core': 7.0.2 + '@octokit/plugin-paginate-rest': 13.1.1(@octokit/core@7.0.2) + '@octokit/plugin-retry': 8.0.1(@octokit/core@7.0.2) + '@octokit/plugin-throttling': 11.0.1(@octokit/core@7.0.2) + '@semantic-release/error': 4.0.0 + aggregate-error: 5.0.0 + debug: 4.4.1 + dir-glob: 3.0.1 + globby: 14.1.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + issue-parser: 7.0.1 + lodash-es: 4.17.21 + mime: 4.0.7 + p-filter: 4.1.0 + semantic-release: 24.2.6(typescript@5.7.2) + url-join: 5.0.0 + transitivePeerDependencies: + - supports-color + + '@semantic-release/npm@12.0.2(semantic-release@24.2.6(typescript@5.7.2))': + dependencies: + '@semantic-release/error': 4.0.0 + aggregate-error: 5.0.0 + execa: 9.6.0 + fs-extra: 11.3.0 + lodash-es: 4.17.21 + nerf-dart: 1.0.0 + normalize-url: 8.0.2 + npm: 10.9.3 + rc: 1.2.8 + read-pkg: 9.0.1 + registry-auth-token: 5.1.0 + semantic-release: 24.2.6(typescript@5.7.2) + semver: 7.7.1 + tempy: 3.1.0 + + '@semantic-release/release-notes-generator@14.0.3(semantic-release@24.2.6(typescript@5.7.2))': + dependencies: + conventional-changelog-angular: 8.0.0 + conventional-changelog-writer: 8.1.0 + conventional-commits-filter: 5.0.0 + conventional-commits-parser: 6.2.0 + debug: 4.4.1 + get-stream: 7.0.1 + import-from-esm: 2.0.0 + into-stream: 7.0.0 + lodash-es: 4.17.21 + read-package-up: 11.0.0 + semantic-release: 24.2.6(typescript@5.7.2) + transitivePeerDependencies: + - supports-color + + '@sinclair/typebox@0.27.8': {} + + '@sindresorhus/is@4.6.0': {} + + '@sindresorhus/merge-streams@2.3.0': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@swc/core-darwin-arm64@1.9.3': + optional: true + + '@swc/core-darwin-x64@1.9.3': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.9.3': + optional: true + + '@swc/core-linux-arm64-gnu@1.9.3': + optional: true + + '@swc/core-linux-arm64-musl@1.9.3': + optional: true + + '@swc/core-linux-x64-gnu@1.9.3': + optional: true + + '@swc/core-linux-x64-musl@1.9.3': + optional: true + + '@swc/core-win32-arm64-msvc@1.9.3': + optional: true + + '@swc/core-win32-ia32-msvc@1.9.3': + optional: true + + '@swc/core-win32-x64-msvc@1.9.3': + optional: true + + '@swc/core@1.9.3': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.17 + optionalDependencies: + '@swc/core-darwin-arm64': 1.9.3 + '@swc/core-darwin-x64': 1.9.3 + '@swc/core-linux-arm-gnueabihf': 1.9.3 + '@swc/core-linux-arm64-gnu': 1.9.3 + '@swc/core-linux-arm64-musl': 1.9.3 + '@swc/core-linux-x64-gnu': 1.9.3 + '@swc/core-linux-x64-musl': 1.9.3 + '@swc/core-win32-arm64-msvc': 1.9.3 + '@swc/core-win32-ia32-msvc': 1.9.3 + '@swc/core-win32-x64-msvc': 1.9.3 + + '@swc/counter@0.1.3': {} + + '@swc/jest@0.2.37(@swc/core@1.9.3)': + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@swc/core': 1.9.3 + '@swc/counter': 0.1.3 + jsonc-parser: 3.3.1 + + '@swc/types@0.1.17': + dependencies: + '@swc/counter': 0.1.3 + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.0 + + '@types/conventional-commits-parser@5.0.1': + dependencies: + '@types/node': 22.15.29 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.15.29 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@22.15.29': + dependencies: + undici-types: 6.21.0 + + '@types/normalize-package-data@2.4.4': {} + + '@types/semver@7.5.8': {} + + '@types/stack-utils@2.0.3': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@types/yoga-layout@1.9.2': {} + + '@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.15.0(eslint@8.57.1)(typescript@5.7.2) + '@typescript-eslint/scope-manager': 8.15.0 + '@typescript-eslint/type-utils': 8.15.0(eslint@8.57.1)(typescript@5.7.2) + '@typescript-eslint/utils': 8.15.0(eslint@8.57.1)(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.15.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.1(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.15.0 + '@typescript-eslint/types': 8.15.0 + '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.15.0 + debug: 4.3.7 + eslint: 8.57.1 + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/rule-tester@8.15.0(eslint@8.57.1)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.7.2) + '@typescript-eslint/utils': 8.15.0(eslint@8.57.1)(typescript@5.7.2) + ajv: 6.12.6 + eslint: 8.57.1 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + semver: 7.7.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/scope-manager@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + + '@typescript-eslint/scope-manager@8.15.0': + dependencies: + '@typescript-eslint/types': 8.15.0 + '@typescript-eslint/visitor-keys': 8.15.0 + + '@typescript-eslint/type-utils@8.15.0(eslint@8.57.1)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.7.2) + '@typescript-eslint/utils': 8.15.0(eslint@8.57.1)(typescript@5.7.2) + debug: 4.4.1 + eslint: 8.57.1 + ts-api-utils: 1.4.1(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@5.62.0': {} + + '@typescript-eslint/types@8.15.0': {} + + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.7.2)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.4.1 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.7.1 + tsutils: 3.21.0(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.15.0(typescript@5.7.2)': + dependencies: + '@typescript-eslint/types': 8.15.0 + '@typescript-eslint/visitor-keys': 8.15.0 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.1 + ts-api-utils: 1.4.1(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.7.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.7.2) + eslint: 8.57.1 + eslint-scope: 5.1.1 + semver: 7.7.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@8.15.0(eslint@8.57.1)(typescript@5.7.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.15.0 + '@typescript-eslint/types': 8.15.0 + '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.7.2) + eslint: 8.57.1 + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@8.15.0': + dependencies: + '@typescript-eslint/types': 8.15.0 + eslint-visitor-keys: 4.2.0 + + '@ungap/structured-clone@1.2.0': {} + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + agent-base@7.1.3: {} + + aggregate-error@5.0.0: + dependencies: + clean-stack: 5.2.0 + indent-string: 5.0.0 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + argv-formatter@1.0.0: {} + + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + + array-ify@1.0.0: {} + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + + array-union@2.1.0: {} + + array.prototype.findlastindex@1.2.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + + array.prototype.flat@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-shim-unscopables: 1.0.2 + + array.prototype.flatmap@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-shim-unscopables: 1.0.2 + + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + + astral-regex@2.0.0: {} + + auto-bind@4.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + babel-jest@29.7.0(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.26.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.25.9 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.1 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + + babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) + + babel-preset-jest@29.6.3(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + + balanced-match@1.0.2: {} + + before-after-hook@4.0.0: {} + + boolean@3.2.0: {} + + bottleneck@2.19.5: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001684 + electron-to-chromium: 1.5.64 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001684: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.4.1: {} + + char-regex@1.0.2: {} + + ci-info@2.0.0: {} + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.1: {} + + clean-stack@5.2.0: + dependencies: + escape-string-regexp: 5.0.0 + + cli-boxes@2.2.1: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + co@4.6.0: {} + + code-excerpt@3.0.0: + dependencies: + convert-to-spaces: 1.0.2 + + collect-v8-coverage@1.0.2: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + commander@10.0.1: {} + + commander@13.1.0: {} + + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + + concat-map@0.0.1: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + conventional-changelog-angular@7.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-angular@8.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@7.0.2: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-writer@8.1.0: + dependencies: + conventional-commits-filter: 5.0.0 + handlebars: 4.7.8 + meow: 13.2.0 + semver: 7.7.1 + + conventional-commits-filter@5.0.0: {} + + conventional-commits-parser@5.0.0: + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + + conventional-commits-parser@6.2.0: + dependencies: + meow: 13.2.0 + + convert-hrtime@5.0.0: {} + + convert-source-map@2.0.0: {} + + convert-to-spaces@1.0.2: {} + + core-util-is@1.0.3: {} + + cosmiconfig-typescript-loader@6.1.0(@types/node@22.15.29)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2): + dependencies: + '@types/node': 22.15.29 + cosmiconfig: 9.0.0(typescript@5.7.2) + jiti: 2.4.2 + typescript: 5.7.2 + + cosmiconfig@8.3.6(typescript@5.7.2): + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.7.2 + + cosmiconfig@9.0.0(typescript@5.7.2): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.7.2 + + create-jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + create-require@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-random-string@4.0.0: + dependencies: + type-fest: 1.4.0 + + dargs@8.1.0: {} + + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + dedent@1.5.3: {} + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + del-cli@6.0.0: + dependencies: + del: 8.0.0 + meow: 13.2.0 + + del@8.0.0: + dependencies: + globby: 14.0.2 + is-glob: 4.0.3 + is-path-cwd: 3.0.0 + is-path-inside: 4.0.0 + p-map: 7.0.2 + slash: 5.1.0 + + detect-newline@3.1.0: {} + + diff-sequences@29.6.3: {} + + diff@4.0.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dot-prop@7.2.0: + dependencies: + type-fest: 2.19.0 + + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + + electron-to-chromium@1.5.64: {} + + emittery@0.13.1: {} + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + emojilib@2.4.0: {} + + enhanced-resolve@5.17.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + env-ci@11.1.1: + dependencies: + execa: 8.0.1 + java-properties: 1.0.2 + + env-paths@2.2.1: {} + + environment@1.1.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.23.5: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.3 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.3 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.3 + typed-array-length: 1.0.7 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.0.2: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.2.1: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-config-prettier@9.1.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-doc-generator@1.7.1(eslint@8.57.1)(typescript@5.7.2): + dependencies: + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.7.2) + ajv: 8.17.1 + boolean: 3.2.0 + commander: 10.0.1 + cosmiconfig: 8.3.6(typescript@5.7.2) + deepmerge: 4.3.1 + dot-prop: 7.2.0 + eslint: 8.57.1 + jest-diff: 29.7.0 + json-schema-traverse: 1.0.0 + markdown-table: 3.0.4 + no-case: 3.0.4 + type-fest: 3.13.1 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.15.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.3.7 + enhanced-resolve: 5.17.1 + eslint: 8.57.1 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + fast-glob: 3.3.2 + get-tsconfig: 4.8.1 + is-bun-module: 1.2.1 + is-glob: 4.0.3 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.15.0(eslint@8.57.1)(typescript@5.7.2) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + + eslint-plugin-es@3.0.1(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.15.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + string.prototype.trimend: 1.0.8 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.15.0(eslint@8.57.1)(typescript@5.7.2) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jest-formatting@3.1.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-jest@28.12.0(@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)))(typescript@5.7.2): + dependencies: + '@typescript-eslint/utils': 8.15.0(eslint@8.57.1)(typescript@5.7.2) + eslint: 8.57.1 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) + jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-node@11.1.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-plugin-es: 3.0.1(eslint@8.57.1) + eslint-utils: 2.1.0 + ignore: 5.3.2 + minimatch: 3.1.2 + resolve: 1.22.8 + semver: 6.3.1 + + eslint-plugin-promise@7.2.1(eslint@8.57.1): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + eslint: 8.57.1 + + eslint-remote-tester-repositories@1.0.1: {} + + eslint-remote-tester@3.0.1(eslint@8.57.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)): + dependencies: + '@babel/code-frame': 7.26.2 + JSONStream: 1.3.5 + chalk: 4.1.2 + eslint: 8.57.1 + ink: 3.2.0(react@17.0.2) + object-hash: 3.0.0 + react: 17.0.2 + simple-git: 3.27.0 + optionalDependencies: + ts-node: 10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2) + transitivePeerDependencies: + - '@types/react' + - bufferutil + - supports-color + - utf-8-validate + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-utils@2.1.0: + dependencies: + eslint-visitor-keys: 1.3.0 + + eslint-visitor-keys@1.3.0: {} + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.3.7 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + eventemitter3@5.0.1: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.2.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + + exit@0.1.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + fast-content-type-parse@3.0.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.0.3: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + figures@2.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up-simple@1.0.1: {} + + find-up@2.1.0: + dependencies: + locate-path: 2.0.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + find-versions@6.0.0: + dependencies: + semver-regex: 4.0.5 + super-regex: 1.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.2 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.2: {} + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + from2@2.3.0: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function-timeout@1.0.2: {} + + function.prototype.name@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + functions-have-names: 1.2.3 + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.0: {} + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + get-package-type@0.1.0: {} + + get-stream@6.0.1: {} + + get-stream@7.0.1: {} + + get-stream@8.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-symbol-description@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + git-log-parser@1.2.1: + dependencies: + argv-formatter: 1.0.0 + spawn-error-forwarder: 1.0.0 + split2: 1.0.0 + stream-combiner2: 1.1.1 + through2: 2.0.5 + traverse: 0.6.8 + + git-raw-commits@4.0.0: + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + globals@11.12.0: {} + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@14.0.2: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.2 + ignore: 5.3.2 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 + + globby@14.1.0: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + path-type: 6.0.0 + slash: 5.1.0 + unicorn-magic: 0.3.0 + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + + graceful-fs@4.2.10: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-bigints@1.0.2: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + highlight.js@10.7.3: {} + + hook-std@3.0.0: {} + + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + + hosted-git-info@8.1.0: + dependencies: + lru-cache: 10.4.3 + + html-escaper@2.0.2: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + human-signals@5.0.0: {} + + human-signals@8.0.1: {} + + husky@9.1.7: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-from-esm@2.0.0: + dependencies: + debug: 4.4.1 + import-meta-resolve: 4.1.0 + transitivePeerDependencies: + - supports-color + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + import-meta-resolve@4.1.0: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + indent-string@5.0.0: {} + + index-to-position@1.1.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@4.1.1: {} + + ink@3.2.0(react@17.0.2): + dependencies: + ansi-escapes: 4.3.2 + auto-bind: 4.0.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + cli-cursor: 3.1.0 + cli-truncate: 2.1.0 + code-excerpt: 3.0.0 + indent-string: 4.0.0 + is-ci: 2.0.0 + lodash: 4.17.21 + patch-console: 1.0.0 + react: 17.0.2 + react-devtools-core: 4.28.5 + react-reconciler: 0.26.2(react@17.0.2) + scheduler: 0.20.2 + signal-exit: 3.0.7 + slice-ansi: 3.0.0 + stack-utils: 2.0.6 + string-width: 4.2.3 + type-fest: 0.12.0 + widest-line: 3.1.0 + wrap-ansi: 6.2.0 + ws: 7.5.10 + yoga-layout-prebuilt: 1.10.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + + into-stream@7.0.0: + dependencies: + from2: 2.3.0 + p-is-promise: 3.0.0 + + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + is-arrayish@0.2.1: {} + + is-async-function@2.0.0: + dependencies: + has-tostringtag: 1.0.2 + + is-bigint@1.0.4: + dependencies: + has-bigints: 1.0.2 + + is-boolean-object@1.1.2: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-bun-module@1.2.1: + dependencies: + semver: 7.7.1 + + is-callable@1.2.7: {} + + is-ci@2.0.0: + dependencies: + ci-info: 2.0.0 + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.1: + dependencies: + is-typed-array: 1.1.13 + + is-date-object@1.0.5: + dependencies: + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.0: + dependencies: + call-bind: 1.0.7 + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + + is-generator-fn@2.1.0: {} + + is-generator-function@1.0.10: + dependencies: + has-tostringtag: 1.0.2 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-obj@2.0.0: {} + + is-path-cwd@3.0.0: {} + + is-path-inside@3.0.3: {} + + is-path-inside@4.0.0: {} + + is-plain-obj@4.1.0: {} + + is-regex@1.1.4: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.7 + + is-stream@2.0.1: {} + + is-stream@3.0.0: {} + + is-stream@4.0.1: {} + + is-string@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-symbol@1.0.4: + dependencies: + has-symbols: 1.0.3 + + is-text-path@2.0.0: + dependencies: + text-extensions: 2.4.0 + + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + + is-unicode-supported@2.1.0: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.0.2: + dependencies: + call-bind: 1.0.7 + + is-weakset@2.0.3: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + issue-parser@7.0.1: + dependencies: + lodash.capitalize: 4.2.1 + lodash.escaperegexp: 4.1.2 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.uniqby: 4.7.0 + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + java-properties@1.0.2: {} + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.3 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)): + dependencies: + '@babel/core': 7.26.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.15.29 + ts-node: 10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.15.29 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.26.2 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + chalk: 4.1.2 + cjs-module-lexer: 1.4.1 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.26.0 + '@babel/generator': 7.26.2 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.1 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.29 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.15.29 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jiti@2.4.2: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + + json-buffer@3.0.1: {} + + json-parse-better-errors@1.0.2: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonparse@1.3.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lint-staged@15.4.3: + dependencies: + chalk: 5.4.1 + commander: 13.1.0 + debug: 4.4.0 + execa: 8.0.1 + lilconfig: 3.1.3 + listr2: 8.2.5 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.7.0 + transitivePeerDependencies: + - supports-color + + listr2@8.2.5: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + + load-json-file@4.0.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + + locate-path@2.0.0: + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash-es@4.17.21: {} + + lodash.camelcase@4.3.0: {} + + lodash.capitalize@4.2.1: {} + + lodash.escaperegexp@4.1.2: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.kebabcase@4.1.1: {} + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.uniq@4.5.0: {} + + lodash.uniqby@4.7.0: {} + + lodash.upperfirst@4.3.1: {} + + lodash@4.17.21: {} + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.1 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + markdown-table@3.0.4: {} + + marked-terminal@7.3.0(marked@15.0.12): + dependencies: + ansi-escapes: 7.0.0 + ansi-regex: 6.1.0 + chalk: 5.4.1 + cli-highlight: 2.1.11 + cli-table3: 0.6.5 + marked: 15.0.12 + node-emoji: 2.2.0 + supports-hyperlinks: 3.2.0 + + marked@15.0.12: {} + + meow@12.1.1: {} + + meow@13.2.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime@4.0.7: {} + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + nerf-dart@1.0.0: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + + node-int64@0.4.0: {} + + node-releases@2.0.18: {} + + normalize-package-data@6.0.2: + dependencies: + hosted-git-info: 7.0.2 + semver: 7.7.1 + validate-npm-package-license: 3.0.4 + + normalize-path@3.0.0: {} + + normalize-url@8.0.2: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + npm@10.9.3: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.3: {} + + object-keys@1.1.1: {} + + object.assign@4.1.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + + object.values@1.2.0: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-each-series@3.0.0: {} + + p-filter@4.1.0: + dependencies: + p-map: 7.0.3 + + p-is-promise@3.0.0: {} + + p-limit@1.3.0: + dependencies: + p-try: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@2.0.0: + dependencies: + p-limit: 1.3.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-map@7.0.2: {} + + p-map@7.0.3: {} + + p-reduce@3.0.0: {} + + p-try@1.0.0: {} + + p-try@2.2.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@4.0.0: + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.27.1 + index-to-position: 1.1.0 + type-fest: 4.41.0 + + parse-ms@4.0.0: {} + + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + + patch-console@1.0.0: {} + + path-exists@3.0.0: {} + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + path-type@5.0.0: {} + + path-type@6.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pidtree@0.6.0: {} + + pify@3.0.0: {} + + pirates@4.0.6: {} + + pkg-conf@2.1.0: + dependencies: + find-up: 2.1.0 + load-json-file: 4.0.0 + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + possible-typed-array-names@1.0.0: {} + + prelude-ls@1.2.1: {} + + prettier@3.6.2: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + pretty-ms@9.2.0: + dependencies: + parse-ms: 4.0.0 + + process-nextick-args@2.0.1: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + proto-list@1.2.4: {} + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + queue-microtask@1.2.3: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-devtools-core@4.28.5: + dependencies: + shell-quote: 1.8.1 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + react-is@18.3.1: {} + + react-reconciler@0.26.2(react@17.0.2): + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react: 17.0.2 + scheduler: 0.20.2 + + react@17.0.2: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + + read-package-up@11.0.0: + dependencies: + find-up-simple: 1.0.1 + read-pkg: 9.0.1 + type-fest: 4.41.0 + + read-pkg@9.0.1: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 6.0.2 + parse-json: 8.3.0 + type-fest: 4.41.0 + unicorn-magic: 0.1.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + reflect.getprototypeof@1.0.7: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + which-builtin-type: 1.2.0 + + regexp.prototype.flags@1.5.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + regexpp@3.2.0: {} + + registry-auth-token@5.1.0: + dependencies: + '@pnpm/npm-conf': 2.3.1 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve.exports@2.0.2: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.0.4: {} + + rfdc@1.4.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.2: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + + safe-buffer@5.1.2: {} + + safe-regex-test@1.0.3: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + + scheduler@0.20.2: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + + semantic-release@24.2.6(typescript@5.7.2): + dependencies: + '@semantic-release/commit-analyzer': 13.0.1(semantic-release@24.2.6(typescript@5.7.2)) + '@semantic-release/error': 4.0.0 + '@semantic-release/github': 11.0.3(semantic-release@24.2.6(typescript@5.7.2)) + '@semantic-release/npm': 12.0.2(semantic-release@24.2.6(typescript@5.7.2)) + '@semantic-release/release-notes-generator': 14.0.3(semantic-release@24.2.6(typescript@5.7.2)) + aggregate-error: 5.0.0 + cosmiconfig: 9.0.0(typescript@5.7.2) + debug: 4.4.1 + env-ci: 11.1.1 + execa: 9.6.0 + figures: 6.1.0 + find-versions: 6.0.0 + get-stream: 6.0.1 + git-log-parser: 1.2.1 + hook-std: 3.0.0 + hosted-git-info: 8.1.0 + import-from-esm: 2.0.0 + lodash-es: 4.17.21 + marked: 15.0.12 + marked-terminal: 7.3.0(marked@15.0.12) + micromatch: 4.0.8 + p-each-series: 3.0.0 + p-reduce: 3.0.0 + read-package-up: 11.0.0 + resolve-from: 5.0.0 + semver: 7.7.1 + semver-diff: 4.0.0 + signale: 1.4.0 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + - typescript + + semver-diff@4.0.0: + dependencies: + semver: 7.7.1 + + semver-regex@4.0.5: {} + + semver@6.3.1: {} + + semver@7.7.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.1: {} + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.3 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + signale@1.4.0: + dependencies: + chalk: 2.4.2 + figures: 2.0.0 + pkg-conf: 2.1.0 + + simple-git@3.27.0: + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + sisteransi@1.0.5: {} + + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + + slash@3.0.0: {} + + slash@5.1.0: {} + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + spawn-error-forwarder@1.0.0: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.21 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.21 + + spdx-license-ids@3.0.21: {} + + split2@1.0.0: + dependencies: + through2: 2.0.5 + + split2@4.2.0: {} + + sprintf-js@1.0.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stream-combiner2@1.1.1: + dependencies: + duplexer2: 0.1.4 + readable-stream: 2.3.8 + + string-argv@0.3.2: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + super-regex@1.0.0: + dependencies: + function-timeout: 1.0.2 + time-span: 5.1.0 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tapable@2.2.1: {} + + temp-dir@3.0.0: {} + + tempy@3.1.0: + dependencies: + is-stream: 3.0.0 + temp-dir: 3.0.0 + type-fest: 2.19.0 + unique-string: 3.0.0 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-extensions@2.4.0: {} + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + + through@2.3.8: {} + + time-span@5.1.0: + dependencies: + convert-hrtime: 5.0.0 + + tinyexec@0.3.2: {} + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + traverse@0.6.8: {} + + ts-api-utils@1.4.1(typescript@5.7.2): + dependencies: + typescript: 5.7.2 + + ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.15.29 + acorn: 8.14.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.7.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.9.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsutils@3.21.0(typescript@5.7.2): + dependencies: + tslib: 1.14.1 + typescript: 5.7.2 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.12.0: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@1.4.0: {} + + type-fest@2.19.0: {} + + type-fest@3.13.1: {} + + type-fest@4.41.0: {} + + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-byte-offset@1.0.3: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + reflect.getprototypeof: 1.0.7 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + reflect.getprototypeof: 1.0.7 + + typescript@5.7.2: {} + + uglify-js@3.19.3: + optional: true + + unbox-primitive@1.0.2: + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + + undici-types@6.21.0: {} + + unicode-emoji-modifier-base@1.0.0: {} + + unicorn-magic@0.1.0: {} + + unicorn-magic@0.3.0: {} + + unique-string@3.0.0: + dependencies: + crypto-random-string: 4.0.0 + + universal-user-agent@7.0.3: {} + + universalify@2.0.1: {} + + update-browserslist-db@1.1.1(browserslist@4.24.2): + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-join@5.0.0: {} + + util-deprecate@1.0.2: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + which-boxed-primitive@1.0.2: + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + + which-builtin-type@1.2.0: + dependencies: + call-bind: 1.0.7 + function.prototype.name: 1.1.6 + has-tostringtag: 1.0.2 + is-async-function: 2.0.0 + is-date-object: 1.0.5 + is-finalizationregistry: 1.1.0 + is-generator-function: 1.0.10 + is-regex: 1.1.4 + is-weakref: 1.0.2 + isarray: 2.0.5 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.2 + which-typed-array: 1.1.15 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.3 + + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + ws@7.5.10: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@2.7.0: {} + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.1: {} + + yoctocolors@2.1.1: {} + + yoga-layout-prebuilt@1.10.0: + dependencies: + '@types/yoga-layout': 1.9.2 diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap deleted file mode 100644 index d800dee4..00000000 --- a/tests/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,111 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should have run 'generate:configs' script when changing config rules 1`] = ` -Object { - "angular": Object { - "plugins": Array [ - "testing-library", - ], - "rules": Object { - "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-debugging-utils": "error", - "testing-library/no-dom-import": Array [ - "error", - "angular", - ], - "testing-library/no-node-access": "error", - "testing-library/no-promise-in-fire-event": "error", - "testing-library/no-render-in-setup": "error", - "testing-library/no-wait-for-empty-callback": "error", - "testing-library/no-wait-for-multiple-assertions": "error", - "testing-library/no-wait-for-side-effects": "error", - "testing-library/no-wait-for-snapshot": "error", - "testing-library/prefer-find-by": "error", - "testing-library/prefer-presence-queries": "error", - "testing-library/prefer-query-by-disappearance": "error", - "testing-library/prefer-screen-queries": "error", - "testing-library/render-result-naming-convention": "error", - }, - }, - "dom": Object { - "plugins": Array [ - "testing-library", - ], - "rules": Object { - "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/no-wait-for-multiple-assertions": "error", - "testing-library/no-wait-for-side-effects": "error", - "testing-library/no-wait-for-snapshot": "error", - "testing-library/prefer-find-by": "error", - "testing-library/prefer-presence-queries": "error", - "testing-library/prefer-query-by-disappearance": "error", - "testing-library/prefer-screen-queries": "error", - }, - }, - "react": Object { - "plugins": Array [ - "testing-library", - ], - "rules": Object { - "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-debugging-utils": "error", - "testing-library/no-dom-import": Array [ - "error", - "react", - ], - "testing-library/no-node-access": "error", - "testing-library/no-promise-in-fire-event": "error", - "testing-library/no-render-in-setup": "error", - "testing-library/no-unnecessary-act": "error", - "testing-library/no-wait-for-empty-callback": "error", - "testing-library/no-wait-for-multiple-assertions": "error", - "testing-library/no-wait-for-side-effects": "error", - "testing-library/no-wait-for-snapshot": "error", - "testing-library/prefer-find-by": "error", - "testing-library/prefer-presence-queries": "error", - "testing-library/prefer-query-by-disappearance": "error", - "testing-library/prefer-screen-queries": "error", - "testing-library/render-result-naming-convention": "error", - }, - }, - "vue": Object { - "plugins": Array [ - "testing-library", - ], - "rules": Object { - "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-debugging-utils": "error", - "testing-library/no-dom-import": Array [ - "error", - "vue", - ], - "testing-library/no-node-access": "error", - "testing-library/no-promise-in-fire-event": "error", - "testing-library/no-render-in-setup": "error", - "testing-library/no-wait-for-empty-callback": "error", - "testing-library/no-wait-for-multiple-assertions": "error", - "testing-library/no-wait-for-side-effects": "error", - "testing-library/no-wait-for-snapshot": "error", - "testing-library/prefer-find-by": "error", - "testing-library/prefer-presence-queries": "error", - "testing-library/prefer-query-by-disappearance": "error", - "testing-library/prefer-screen-queries": "error", - "testing-library/render-result-naming-convention": "error", - }, - }, -} -`; diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 8861cafd..c6e72252 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -4,145 +4,145 @@ import { createRuleTester } from './lib/test-utils'; const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { - valid: [ - // Test Cases for Imports - { - code: ` + valid: [ + // Test Cases for Imports + { + code: ` // case: nothing related to Testing Library at all import { shallow } from 'enzyme'; - + const wrapper = shallow(); `, - }, - { - code: ` + }, + { + code: ` // case: nothing related to Testing Library at all (require version) const { shallow } = require('enzyme'); - + const wrapper = shallow(); `, - }, - { - code: ` + }, + { + code: ` // case: render imported from other than custom module import { render } from '@somewhere/else' - + const utils = render(); `, - settings: { - 'testing-library/utils-module': 'test-utils', - }, - }, - { - code: ` + settings: { + 'testing-library/utils-module': 'test-utils', + }, + }, + { + code: ` // case: render imported from other than custom module (require version) const { render } = require('@somewhere/else') - + const utils = render(); `, - settings: { - 'testing-library/utils-module': 'test-utils', - }, - }, - { - code: ` + settings: { + 'testing-library/utils-module': 'test-utils', + }, + }, + { + code: ` // case: prevent import which should trigger an error since it's imported // from other than settings custom module import { foo } from 'report-me' `, - settings: { - 'testing-library/utils-module': 'test-utils', - }, - }, - { - code: ` + settings: { + 'testing-library/utils-module': 'test-utils', + }, + }, + { + code: ` // case: prevent import which should trigger an error since it's imported // from other than settings custom module (require version) const { foo } = require('report-me') `, - settings: { - 'testing-library/utils-module': 'test-utils', - }, - }, - { - code: ` + settings: { + 'testing-library/utils-module': 'test-utils', + }, + }, + { + code: ` // case: import custom module forced to be reported without custom module setting import { foo } from 'custom-module-forced-report' `, - }, - { - settings: { - 'testing-library/utils-module': 'off', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'off', + }, + code: ` // case: aggressive import switched off - imported from non-built-in module import 'report-me'; require('report-me'); `, - }, + }, - // Test Cases for user-event imports - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + // Test Cases for user-event imports + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import userEvent from 'somewhere-else' userEvent.click(element) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import '@testing-library/user-event' userEvent.click() `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { click } from '@testing-library/user-event' userEvent.click() `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import * as incorrect from '@testing-library/user-event' userEvent.click() `, - }, + }, - // Test Cases for renders - { - code: ` + // Test Cases for renders + { + code: ` // case: aggressive render enabled - method not containing "render" import { somethingElse } from '@somewhere/else' - + const utils = somethingElse() `, - }, - { - settings: { 'testing-library/custom-renders': ['renderWithRedux'] }, - code: ` + }, + { + settings: { 'testing-library/custom-renders': ['renderWithRedux'] }, + code: ` // case: aggressive render disabled - method not matching valid render import { customRender } from '@somewhere/else' - + const utils = customRender() `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` // case: aggressive render enabled, but module disabled - not coming from TL import { render } from 'somewhere-else' - + const utils = render() `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case (render util): aggressive reporting disabled - method with same name // as TL method but not coming from TL module is valid import { render as testingLibraryRender } from 'test-utils' @@ -150,177 +150,176 @@ ruleTester.run(RULE_NAME, rule, { const utils = render() `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` // case: aggressive module disabled and render coming from non-related module import * as somethingElse from '@somewhere/else' import { render } from '@testing-library/react' - + // somethingElse.render is not coming from any module related to TL const utils = somethingElse.render() `, - }, - { - settings: { - 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], - }, - code: ` + }, + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, + code: ` // case: aggressive render disabled - method not matching custom-renders import { renderWithProviders } from '@somewhere/else' - + const utils = renderWithProviders() `, - }, - { - settings: { - 'testing-library/custom-renders': 'off', - }, - code: ` + }, + { + settings: { + 'testing-library/custom-renders': 'off', + }, + code: ` // case: aggressive render switched off import { renderWithProviders } from '@somewhere/else' - + const utils = renderWithProviders() `, - }, + }, - // Test Cases for presence/absence assertions - // cases: asserts not related to presence/absence - 'expect(element).toBeDisabled()', - 'expect(element).toBeEnabled()', + // Test Cases for presence/absence assertions + // cases: asserts not related to presence/absence + 'expect(element).toBeDisabled()', + 'expect(element).toBeEnabled()', - // cases: presence/absence matcher not related to assert - 'element.toBeInTheDocument()', - 'element.not.toBeInTheDocument()', + // cases: presence/absence matcher not related to assert + 'element.toBeInTheDocument()', + 'element.not.toBeInTheDocument()', - // cases: weird scenarios to check guard against parent nodes - 'expect(element).not()', - 'expect(element).not()', + // cases: weird scenarios to check guard against parent nodes + 'expect(element).not()', - // Test Cases for Queries and Aggressive Queries Reporting - { - code: ` + // Test Cases for Queries and Aggressive Queries Reporting + { + code: ` // case: custom method not matching "getBy*" variant pattern getSomeElement('button') `, - }, - { - code: ` + }, + { + code: ` // case: custom method not matching "getBy*" variant pattern using within within(container).getSomeElement('button') `, - }, - { - code: ` + }, + { + code: ` // case: custom method not matching "queryBy*" variant pattern querySomeElement('button') `, - }, - { - code: ` + }, + { + code: ` // case: custom method not matching "queryBy*" variant pattern using within within(container).querySomeElement('button') `, - }, - { - code: ` + }, + { + code: ` // case: custom method not matching "findBy*" variant pattern findSomeElement('button') `, - }, - { - code: ` + }, + { + code: ` // case: custom method not matching "findBy*" variant pattern using within within(container).findSomeElement('button') `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: built-in "getBy*" query not reported because custom module not imported import { render } from 'other-module' getByRole('button') `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: built-in "getBy*" query not reported because custom module not imported using within import { render } from 'other-module' within(container).getByRole('button') `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: built-in "queryBy*" query not reported because custom module not imported import { render } from 'other-module' queryByRole('button') `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: built-in "queryBy*" query not reported because custom module not imported using within import { render } from 'other-module' within(container).queryByRole('button') `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: built-in "findBy*" query not reported because custom module not imported import { render } from 'other-module' findByRole('button') `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: built-in "findBy*" query not reported because custom module not imported using within import { render } from 'other-module' within(container).findByRole('button') `, - }, - { - settings: { - 'testing-library/custom-queries': ['ByComplexText', 'findByIcon'], - }, - code: `// case: custom "queryBy*" query not reported (custom-queries not matching) + }, + { + settings: { + 'testing-library/custom-queries': ['ByComplexText', 'findByIcon'], + }, + code: `// case: custom "queryBy*" query not reported (custom-queries not matching) queryByIcon('search')`, - }, - { - settings: { - 'testing-library/custom-queries': ['ByComplexText', 'queryByIcon'], - }, - code: `// case: custom "getBy*" query not reported (custom-queries not matching) + }, + { + settings: { + 'testing-library/custom-queries': ['ByComplexText', 'queryByIcon'], + }, + code: `// case: custom "getBy*" query not reported (custom-queries not matching) getByIcon('search')`, - }, - { - settings: { - 'testing-library/custom-queries': ['ByComplexText', 'getByIcon'], - }, - code: `// case: custom "findBy*" query not reported (custom-queries not matching) + }, + { + settings: { + 'testing-library/custom-queries': ['ByComplexText', 'getByIcon'], + }, + code: `// case: custom "findBy*" query not reported (custom-queries not matching) findByIcon('search')`, - }, - { - settings: { - 'testing-library/custom-queries': 'off', - }, - code: `// case: custom queries not reported (aggressive queries switched off) + }, + { + settings: { + 'testing-library/custom-queries': 'off', + }, + code: `// case: custom queries not reported (aggressive queries switched off) getByIcon('search'); queryByIcon('search'); findByIcon('search'); @@ -328,26 +327,26 @@ ruleTester.run(RULE_NAME, rule, { queryAllByIcon('search'); findAllByIcon('search'); `, - }, + }, - // Test Cases for async utils - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + // Test Cases for async utils + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { waitFor } from 'some-other-library'; test( 'aggressive reporting disabled - util waitFor not related to testing library is valid', () => { waitFor() } ); `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case (async util): aggressive reporting disabled - method with same name // as TL method but not coming from TL module is valid import { waitFor as testingLibraryWaitFor } from 'test-utils' @@ -357,707 +356,710 @@ ruleTester.run(RULE_NAME, rule, { waitFor() }); `, - }, + }, - // Test Cases for all settings mixed - { - settings: { - 'testing-library/utils-module': 'test-utils', - 'testing-library/custom-renders': ['customRender'], - 'testing-library/custom-queries': ['ByIcon', 'ByComplexText'], - }, - code: ` + // Test Cases for all settings mixed + { + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['customRender'], + 'testing-library/custom-queries': ['ByIcon', 'ByComplexText'], + }, + code: ` // case: not matching any of the custom settings import { renderWithRedux } from 'test-utils' import { render } from 'other-utils' import { somethingElse } from 'another-module' const foo = require('bar') - + const utils = render() renderWithRedux() getBySomethingElse('foo') queryBySomethingElse('foo') findBySomethingElse('foo') `, - }, - { - settings: { - 'testing-library/utils-module': 'off', - 'testing-library/custom-renders': 'off', - 'testing-library/custom-queries': 'off', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'off', + 'testing-library/custom-renders': 'off', + 'testing-library/custom-queries': 'off', + }, + code: ` // case: all settings switched off + only custom utils used import { renderWithRedux } from 'test-utils' import { render } from 'other-utils' import { somethingElse } from 'another-module' const foo = require('bar') - + const utils = render() renderWithRedux() getBySomethingElse('foo') queryBySomethingElse('foo') findBySomethingElse('foo') `, - }, + }, - // Weird edge cases - `(window as any).__THING = false;`, - `thing.method.lastCall.args[0]();`, + // Weird edge cases + `(window as any).__THING = false;`, + `thing.method.lastCall.args[0]();`, - `// edge case when setting jest-dom up in jest config file - using require + `// edge case when setting jest-dom up in jest config file - using require require('@testing-library/jest-dom') - + foo() `, - `// edge case when setting jest-dom up in jest config file - using import + `// edge case when setting jest-dom up in jest config file - using import import '@testing-library/jest-dom' - + foo() `, - ], - invalid: [ - // Test Cases for Imports - { - code: ` + ], + invalid: [ + // Test Cases for Imports + { + code: ` // case: import module forced to be reported import { foo } from 'report-me' `, - errors: [{ line: 3, column: 7, messageId: 'fakeError' }], - }, - { - code: ` + errors: [{ line: 3, column: 7, messageId: 'fakeError' }], + }, + { + code: ` // case: render imported from any module by default (aggressive reporting) import { render } from '@somewhere/else' import { somethingElse } from 'another-module' - + const utils = render(); `, - errors: [ - { - line: 6, - column: 21, - messageId: 'renderError', - }, - ], - }, - { - code: ` + errors: [ + { + line: 6, + column: 21, + messageId: 'renderError', + }, + ], + }, + ...['@testing-library/react', '@marko/testing-library'].map( + (testingFramework) => + ({ + code: ` // case: render imported from Testing Library module - import { render } from '@testing-library/react' + import { render } from '${testingFramework}' import { somethingElse } from 'another-module' const foo = require('bar') - + const utils = render(); `, - errors: [ - { - line: 7, - column: 21, - messageId: 'renderError', - }, - ], - }, - { - code: ` + errors: [ + { + line: 7, + column: 21, + messageId: 'renderError', + }, + ], + }) as const + ), + { + code: ` // case: render imported from Testing Library module (require version) const { render } = require('@testing-library/react') import { somethingElse } from 'another-module' const foo = require('bar') - + const utils = render(); `, - errors: [ - { - line: 7, - column: 21, - messageId: 'renderError', - }, - ], - }, - { - code: ` + errors: [ + { + line: 7, + column: 21, + messageId: 'renderError', + }, + ], + }, + { + code: ` // case: render imported from settings custom module import { render } from 'test-utils' import { somethingElse } from 'another-module' const foo = require('bar') - + const utils = render(); `, - settings: { - 'testing-library/utils-module': 'test-utils', - }, - errors: [ - { - line: 7, - column: 21, - messageId: 'renderError', - }, - ], - }, - { - code: ` + settings: { + 'testing-library/utils-module': 'test-utils', + }, + errors: [ + { + line: 7, + column: 21, + messageId: 'renderError', + }, + ], + }, + { + code: ` // case: render imported from settings custom module (require version) const { render } = require('test-utils') import { somethingElse } from 'another-module' const foo = require('bar') - + const utils = render(); `, - settings: { - 'testing-library/utils-module': 'test-utils', - }, - errors: [ - { - line: 7, - column: 21, - messageId: 'renderError', - }, - ], - }, - { - code: ` + settings: { + 'testing-library/utils-module': 'test-utils', + }, + errors: [ + { + line: 7, + column: 21, + messageId: 'renderError', + }, + ], + }, + { + code: ` // case: render imported from Testing Library module with // settings custom module import { render } from '@testing-library/react' import { somethingElse } from 'another-module' const foo = require('bar') - + const utils = render(); `, - settings: { - 'testing-library/utils-module': 'test-utils', - }, - errors: [ - { - line: 8, - column: 21, - messageId: 'renderError', - }, - ], - }, - { - code: ` + settings: { + 'testing-library/utils-module': 'test-utils', + }, + errors: [ + { + line: 8, + column: 21, + messageId: 'renderError', + }, + ], + }, + { + code: ` // case: render imported from Testing Library module with // settings custom module (require version) const { render } = require('@testing-library/react') import { somethingElse } from 'another-module' const foo = require('bar') - + const utils = render(); `, - settings: { - 'testing-library/utils-module': 'test-utils', - }, - errors: [ - { - line: 8, - column: 21, - messageId: 'renderError', - }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'custom-module-forced-report', - }, - code: ` + settings: { + 'testing-library/utils-module': 'test-utils', + }, + errors: [ + { + line: 8, + column: 21, + messageId: 'renderError', + }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'custom-module-forced-report', + }, + code: ` // case: import custom module forced to be reported with custom module setting import { foo } from 'custom-module-forced-report' `, - errors: [{ line: 3, column: 7, messageId: 'fakeError' }], - }, + errors: [{ line: 3, column: 7, messageId: 'fakeError' }], + }, - // Test Cases for user-event imports - { - code: ` + // Test Cases for user-event imports + { + code: ` import userEvent from 'somewhere-else' userEvent.click(element) `, - errors: [{ line: 3, column: 17, messageId: 'userEventError' }], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import userEvent from '@testing-library/user-event' userEvent.click(element) `, - errors: [{ line: 3, column: 17, messageId: 'userEventError' }], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import renamed from '@testing-library/user-event' renamed.click(element) `, - errors: [{ line: 3, column: 15, messageId: 'userEventError' }], - }, - { - code: ` + errors: [{ line: 3, column: 15, messageId: 'userEventError' }], + }, + { + code: ` const userEvent = require('somewhere-else') userEvent.click(element) `, - errors: [{ line: 3, column: 17, messageId: 'userEventError' }], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` const userEvent = require('@testing-library/user-event') userEvent.click(element) `, - errors: [{ line: 3, column: 17, messageId: 'userEventError' }], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` const renamed = require('@testing-library/user-event') renamed.click(element) `, - errors: [{ line: 3, column: 15, messageId: 'userEventError' }], - }, + errors: [{ line: 3, column: 15, messageId: 'userEventError' }], + }, - // Test Cases for renders - { - code: ` + // Test Cases for renders + { + code: ` // case: aggressive render enabled - Testing Library render import { render } from '@testing-library/react' - + const utils = render() `, - errors: [{ line: 5, column: 21, messageId: 'renderError' }], - }, - { - code: ` + errors: [{ line: 5, column: 21, messageId: 'renderError' }], + }, + { + code: ` // case: aggressive render enabled - Testing Library render wildcard imported import * as rtl from '@testing-library/react' - + const utils = rtl.render() `, - errors: [{ line: 5, column: 25, messageId: 'renderError' }], - }, - { - code: ` + errors: [{ line: 5, column: 25, messageId: 'renderError' }], + }, + { + code: ` // case: aggressive render enabled - any method containing "render" import { someRender } from '@somewhere/else' - + const utils = someRender() `, - errors: [{ line: 5, column: 21, messageId: 'renderError' }], - }, - { - settings: { 'testing-library/custom-renders': ['customRender'] }, - code: ` + errors: [{ line: 5, column: 21, messageId: 'renderError' }], + }, + { + settings: { 'testing-library/custom-renders': ['customRender'] }, + code: ` // case: aggressive render disabled - Testing Library render import { render } from '@testing-library/react' - + const utils = render() `, - errors: [{ line: 5, column: 21, messageId: 'renderError' }], - }, - { - settings: { - 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], - }, - code: ` + errors: [{ line: 5, column: 21, messageId: 'renderError' }], + }, + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, + code: ` // case: aggressive render disabled - valid custom render import { customRender } from 'test-utils' - + const utils = customRender() `, - errors: [{ line: 5, column: 21, messageId: 'renderError' }], - }, - { - settings: { - 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], - }, - code: ` + errors: [{ line: 5, column: 21, messageId: 'renderError' }], + }, + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, + code: ` // case: aggressive render disabled - default render from custom module import { render } from 'test-utils' - + const utils = render() `, - errors: [{ line: 5, column: 21, messageId: 'renderError' }], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [{ line: 5, column: 21, messageId: 'renderError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` // case: aggressive module disabled and render wildcard-imported from related module import * as rtl from '@testing-library/react' - + const utils = rtl.render() `, - errors: [{ line: 5, column: 25, messageId: 'renderError' }], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ line: 5, column: 25, messageId: 'renderError' }], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: matching all custom settings import { render } from 'test-utils' import { somethingElse } from 'another-module' const foo = require('bar') - + const utils = render(); `, - errors: [{ line: 7, column: 21, messageId: 'renderError' }], - }, + errors: [{ line: 7, column: 21, messageId: 'renderError' }], + }, - // Test Cases for presence/absence assertions - { - code: ` + // Test Cases for presence/absence assertions + { + code: ` // case: presence matcher .toBeInTheDocument forced to be reported expect(element).toBeInTheDocument() `, - errors: [{ line: 3, column: 7, messageId: 'presenceAssertError' }], - }, - { - code: ` + errors: [{ line: 3, column: 7, messageId: 'presenceAssertError' }], + }, + { + code: ` // case: absence matcher .not.toBeInTheDocument forced to be reported expect(element).not.toBeInTheDocument() `, - errors: [{ line: 3, column: 7, messageId: 'absenceAssertError' }], - }, - { - code: ` + errors: [{ line: 3, column: 7, messageId: 'absenceAssertError' }], + }, + { + code: ` // case: presence matcher .not.toBeNull forced to be reported expect(element).not.toBeNull() `, - errors: [{ line: 3, column: 7, messageId: 'presenceAssertError' }], - }, - { - code: ` + errors: [{ line: 3, column: 7, messageId: 'presenceAssertError' }], + }, + { + code: ` // case: absence matcher .toBeNull forced to be reported expect(element).toBeNull() `, - errors: [{ line: 3, column: 7, messageId: 'absenceAssertError' }], - }, + errors: [{ line: 3, column: 7, messageId: 'absenceAssertError' }], + }, - // Test Cases for async utils - { - code: ` + // Test Cases for async utils + { + code: ` import { waitFor } from 'test-utils'; test( 'aggressive reporting enabled - util waitFor reported no matter where is coming from', () => { waitFor() } ); `, - errors: [ - { - line: 5, - column: 19, - messageId: 'asyncUtilError', - data: { utilName: 'waitFor' }, - }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + line: 5, + column: 19, + messageId: 'asyncUtilError', + data: { utilName: 'waitFor' }, + }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { waitFor } from 'test-utils'; test( 'aggressive reporting disabled - util waitFor related to testing library', () => { waitFor() } ); `, - errors: [ - { - line: 5, - column: 19, - messageId: 'asyncUtilError', - data: { utilName: 'waitFor' }, - }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: aggressive reporting disabled - waitFor from wildcard import related to TL + errors: [ + { + line: 5, + column: 19, + messageId: 'asyncUtilError', + data: { utilName: 'waitFor' }, + }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case: aggressive reporting disabled - waitFor from wildcard import related to TL import * as tl from 'test-utils' tl.waitFor(() => {}) `, - errors: [ - { - line: 4, - column: 12, - messageId: 'asyncUtilError', - data: { utilName: 'waitFor' }, - }, - ], - }, + errors: [ + { + line: 4, + column: 12, + messageId: 'asyncUtilError', + data: { utilName: 'waitFor' }, + }, + ], + }, - // Test Cases for Queries and Aggressive Queries Reporting - { - code: ` + // Test Cases for Queries and Aggressive Queries Reporting + { + code: ` // case: built-in "getBy*" query reported without import (aggressive reporting) getByRole('button') `, - errors: [{ line: 3, column: 7, messageId: 'getByError' }], - }, - { - code: ` + errors: [{ line: 3, column: 7, messageId: 'getByError' }], + }, + { + code: ` // case: built-in "getBy*" query reported without import using within (aggressive reporting) within(container).getByRole('button') `, - errors: [{ line: 3, column: 25, messageId: 'getByError' }], - }, - { - code: ` + errors: [{ line: 3, column: 25, messageId: 'getByError' }], + }, + { + code: ` // case: built-in "queryBy*" query reported without import (aggressive reporting) queryByRole('button') `, - errors: [{ line: 3, column: 7, messageId: 'queryByError' }], - }, - { - code: ` + errors: [{ line: 3, column: 7, messageId: 'queryByError' }], + }, + { + code: ` // case: built-in "queryBy*" query reported without import using within (aggressive reporting) within(container).queryByRole('button') `, - errors: [{ line: 3, column: 25, messageId: 'queryByError' }], - }, - { - code: ` + errors: [{ line: 3, column: 25, messageId: 'queryByError' }], + }, + { + code: ` // case: built-in "findBy*" query reported without import (aggressive reporting) findByRole('button') `, - errors: [{ line: 3, column: 7, messageId: 'findByError' }], - }, - { - code: ` + errors: [{ line: 3, column: 7, messageId: 'findByError' }], + }, + { + code: ` // case: built-in "findBy*" query reported without import using within (aggressive reporting) within(container).findByRole('button') `, - errors: [{ line: 3, column: 25, messageId: 'findByError' }], - }, - { - settings: { - 'testing-library/custom-queries': ['ByIcon'], - }, - code: ` + errors: [{ line: 3, column: 25, messageId: 'findByError' }], + }, + { + settings: { + 'testing-library/custom-queries': ['ByIcon'], + }, + code: ` // case: built-in "queryBy*" query reported (aggressive reporting disabled) queryByRole('button') `, - errors: [{ line: 3, column: 7, messageId: 'queryByError' }], - }, - { - settings: { - 'testing-library/custom-queries': ['ByIcon'], - }, - code: ` + errors: [{ line: 3, column: 7, messageId: 'queryByError' }], + }, + { + settings: { + 'testing-library/custom-queries': ['ByIcon'], + }, + code: ` // case: built-in "queryBy*" query reported (aggressive reporting disabled) within(container).queryByRole('button') `, - errors: [{ line: 3, column: 25, messageId: 'queryByError' }], - }, - { - settings: { - 'testing-library/custom-queries': ['ByIcon'], - }, - code: ` + errors: [{ line: 3, column: 25, messageId: 'queryByError' }], + }, + { + settings: { + 'testing-library/custom-queries': ['ByIcon'], + }, + code: ` // case: built-in "findBy*" query reported (aggressive reporting disabled) findByRole('button') `, - errors: [{ line: 3, column: 7, messageId: 'findByError' }], - }, - { - code: ` + errors: [{ line: 3, column: 7, messageId: 'findByError' }], + }, + { + code: ` // case: custom "queryBy*" query reported without import (aggressive reporting) queryByIcon('search') `, - errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], - }, - { - code: ` + errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], + }, + { + code: ` // case: custom "queryBy*" query reported without import using within (aggressive reporting) within(container).queryByIcon('search') `, - errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], - }, - { - code: ` + errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], + }, + { + code: ` // case: custom "findBy*" query reported without import (aggressive reporting) findByIcon('search') `, - errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], - }, - { - code: ` + errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], + }, + { + code: ` // case: custom "findBy*" query reported without import using within (aggressive reporting) within(container).findByIcon('search') `, - errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], - }, - { - settings: { - 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'], - }, - code: ` + errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], + }, + { + settings: { + 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'], + }, + code: ` // case: custom "queryBy*" query reported without import (custom-queries set) queryByIcon('search') `, - errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], - }, - { - settings: { - 'testing-library/custom-queries': ['ByIcon', 'ByComplexText'], - }, - code: ` + errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], + }, + { + settings: { + 'testing-library/custom-queries': ['ByIcon', 'ByComplexText'], + }, + code: ` // case: custom "queryBy*" query reported without import using within (custom-queries set) within(container).queryByIcon('search') `, - errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], - }, - { - settings: { - 'testing-library/custom-queries': [ - 'queryByIcon', - 'ByComplexText', - 'findByIcon', - ], - }, - code: ` + errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], + }, + { + settings: { + 'testing-library/custom-queries': [ + 'queryByIcon', + 'ByComplexText', + 'findByIcon', + ], + }, + code: ` // case: custom "findBy*" query reported without import (custom-queries set) findByIcon('search') `, - errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], - }, - { - settings: { - 'testing-library/custom-queries': ['ByIcon', 'ByComplexText'], - }, - code: ` + errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], + }, + { + settings: { + 'testing-library/custom-queries': ['ByIcon', 'ByComplexText'], + }, + code: ` // case: custom "findBy*" query reported without import using within (custom-queries set) within(container).findByIcon('search') `, - errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: built-in "getBy*" query reported with custom module + Testing Library package import import { render } from '@testing-library/react' getByRole('button') `, - errors: [{ line: 4, column: 7, messageId: 'getByError' }], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ line: 4, column: 7, messageId: 'getByError' }], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: built-in "getBy*" query reported with custom module + custom module import import { render } from 'test-utils' getByRole('button') `, - errors: [{ line: 4, column: 7, messageId: 'getByError' }], - }, + errors: [{ line: 4, column: 7, messageId: 'getByError' }], + }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: custom "getBy*" query reported with custom module + Testing Library package import import { render } from '@testing-library/react' getByIcon('search') `, - errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: custom "getBy*" query reported with custom module + custom module import import { render } from 'test-utils' getByIcon('search') `, - errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], - }, + errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], + }, - // Test Cases for all settings mixed - { - settings: { - 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], - 'testing-library/utils-module': 'test-utils', - 'testing-library/custom-queries': ['ByIcon', 'findByComplexText'], - }, - code: ` + // Test Cases for all settings mixed + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-queries': ['ByIcon', 'findByComplexText'], + }, + code: ` // case: aggressive reporting disabled - matching all custom settings import { renderWithRedux, waitFor, screen } from 'test-utils' import { findByComplexText } from 'custom-queries' - + const { getByRole, getAllByIcon } = renderWithRedux() const el = getByRole('button') const iconButtons = getAllByIcon('search') waitFor(() => {}) findByComplexText('foo') - + `, - errors: [ - { line: 6, column: 43, messageId: 'renderError' }, - { line: 7, column: 18, messageId: 'getByError' }, - { line: 8, column: 27, messageId: 'customQueryError' }, - { - line: 9, - column: 7, - messageId: 'asyncUtilError', - data: { utilName: 'waitFor' }, - }, - { line: 10, column: 7, messageId: 'customQueryError' }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'off', - 'testing-library/custom-renders': 'off', - 'testing-library/custom-queries': 'off', - }, - code: ` + errors: [ + { line: 6, column: 43, messageId: 'renderError' }, + { line: 7, column: 18, messageId: 'getByError' }, + { line: 8, column: 27, messageId: 'customQueryError' }, + { + line: 9, + column: 7, + messageId: 'asyncUtilError', + data: { utilName: 'waitFor' }, + }, + { line: 10, column: 7, messageId: 'customQueryError' }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'off', + 'testing-library/custom-renders': 'off', + 'testing-library/custom-queries': 'off', + }, + code: ` // case: built-in utils reported when all aggressive reporting completely switched off import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event' - + const utils = render(); const el = utils.getByText('foo'); screen.findByRole('button'); waitFor(); userEvent.click(el); `, - errors: [ - { - line: 6, - column: 21, - messageId: 'renderError', - }, - { - line: 7, - column: 24, - messageId: 'getByError', - }, - { - line: 8, - column: 14, - messageId: 'findByError', - }, - { - line: 9, - column: 7, - messageId: 'asyncUtilError', - }, - { - line: 10, - column: 17, - messageId: 'userEventError', - }, - ], - }, - ], + errors: [ + { + line: 6, + column: 21, + messageId: 'renderError', + }, + { + line: 7, + column: 24, + messageId: 'getByError', + }, + { + line: 8, + column: 14, + messageId: 'findByError', + }, + { + line: 9, + column: 7, + messageId: 'asyncUtilError', + }, + { + line: 10, + column: 17, + messageId: 'userEventError', + }, + ], + }, + ], }); diff --git a/tests/eslint-remote-tester.config.js b/tests/eslint-remote-tester.config.js new file mode 100644 index 00000000..f7c63992 --- /dev/null +++ b/tests/eslint-remote-tester.config.js @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +/* eslint-disable import/no-extraneous-dependencies */ +const { rules } = require('eslint-plugin-testing-library'); +const { + getRepositories, + getPathIgnorePattern, +} = require('eslint-remote-tester-repositories'); + +module.exports = { + repositories: getRepositories({ randomize: true }), + pathIgnorePattern: getPathIgnorePattern(), + extensions: ['js', 'jsx', 'ts', 'tsx'], + concurrentTasks: 3, + cache: false, + logLevel: 'info', + eslintrc: { + root: true, + env: { + es6: true, + }, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + plugins: ['testing-library'], + rules: { + ...Object.keys(rules).reduce( + (all, rule) => ({ + ...all, + [`testing-library/${rule}`]: 'error', + }), + {} + ), + + // Rules with required options without default values + 'testing-library/consistent-data-testid': [ + 'error', + { testIdPattern: '^{fileName}(__([A-Z]+[a-z]_?)+)_$' }, + ], + }, + }, +}; diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index 9f57493e..335eaee5 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -2,131 +2,133 @@ * @file Fake rule to be able to test createTestingLibraryRule and * detectTestingLibraryUtils properly */ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../lib/create-testing-library-rule'; export const RULE_NAME = 'fake-rule'; type Options = []; type MessageIds = - | 'absenceAssertError' - | 'asyncUtilError' - | 'customQueryError' - | 'fakeError' - | 'findByError' - | 'getByError' - | 'presenceAssertError' - | 'queryByError' - | 'renderError' - | 'userEventError'; + | 'absenceAssertError' + | 'asyncUtilError' + | 'customQueryError' + | 'fakeError' + | 'findByError' + | 'getByError' + | 'presenceAssertError' + | 'queryByError' + | 'renderError' + | 'userEventError'; export default createTestingLibraryRule({ - name: RULE_NAME, - meta: { - type: 'problem', - docs: { - description: 'Fake rule to test rule maker and detection helpers', - recommendedConfig: { - dom: false, - angular: false, - react: false, - vue: false, - }, - }, - messages: { - fakeError: 'fake error reported', - renderError: 'some error related to render util reported', - asyncUtilError: - 'some error related to {{ utilName }} async util reported', - getByError: 'some error related to getBy reported', - queryByError: 'some error related to queryBy reported', - findByError: 'some error related to findBy reported', - customQueryError: 'some error related to a customQuery reported', - userEventError: 'some error related to userEvent reported', - presenceAssertError: 'some error related to presence assert reported', - absenceAssertError: 'some error related to absence assert reported', - }, - schema: [], - }, - defaultOptions: [], - create(context, _, helpers) { - const reportCallExpressionIdentifier = (node: TSESTree.Identifier) => { - // force "render" to be reported - if (helpers.isRenderUtil(node)) { - return context.report({ node, messageId: 'renderError' }); - } - - // force async utils to be reported - if (helpers.isAsyncUtil(node)) { - return context.report({ - node, - messageId: 'asyncUtilError', - data: { utilName: node.name }, - }); - } - - if (helpers.isUserEventMethod(node)) { - return context.report({ node, messageId: 'userEventError' }); - } - - // force queries to be reported - if (helpers.isCustomQuery(node)) { - return context.report({ node, messageId: 'customQueryError' }); - } - - if (helpers.isGetQueryVariant(node)) { - return context.report({ node, messageId: 'getByError' }); - } - - if (helpers.isQueryQueryVariant(node)) { - return context.report({ node, messageId: 'queryByError' }); - } - - if (helpers.isFindQueryVariant(node)) { - return context.report({ node, messageId: 'findByError' }); - } - - return undefined; - }; - - const reportMemberExpression = (node: TSESTree.MemberExpression) => { - if (helpers.isPresenceAssert(node)) { - return context.report({ node, messageId: 'presenceAssertError' }); - } - - if (helpers.isAbsenceAssert(node)) { - return context.report({ node, messageId: 'absenceAssertError' }); - } - - return undefined; - }; - - const reportImportDeclaration = (node: TSESTree.ImportDeclaration) => { - // This is just to check that defining an `ImportDeclaration` doesn't - // override `ImportDeclaration` from `detectTestingLibraryUtils` - if (node.source.value === 'report-me') { - context.report({ node, messageId: 'fakeError' }); - } - }; - - return { - 'CallExpression Identifier': reportCallExpressionIdentifier, - MemberExpression: reportMemberExpression, - ImportDeclaration: reportImportDeclaration, - 'Program:exit'() { - const importNode = helpers.getCustomModuleImportNode(); - const importName = helpers.getCustomModuleImportName(); - if (!importNode) { - return; - } - - if (importName === 'custom-module-forced-report') { - context.report({ - node: importNode, - messageId: 'fakeError', - }); - } - }, - }; - }, + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Fake rule to test rule maker and detection helpers', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + svelte: false, + marko: false, + }, + }, + messages: { + fakeError: 'fake error reported', + renderError: 'some error related to render util reported', + asyncUtilError: + 'some error related to {{ utilName }} async util reported', + getByError: 'some error related to getBy reported', + queryByError: 'some error related to queryBy reported', + findByError: 'some error related to findBy reported', + customQueryError: 'some error related to a customQuery reported', + userEventError: 'some error related to userEvent reported', + presenceAssertError: 'some error related to presence assert reported', + absenceAssertError: 'some error related to absence assert reported', + }, + schema: [], + }, + defaultOptions: [], + create(context, _, helpers) { + const reportCallExpressionIdentifier = (node: TSESTree.Identifier) => { + // force "render" to be reported + if (helpers.isRenderUtil(node)) { + return context.report({ node, messageId: 'renderError' }); + } + + // force async utils to be reported + if (helpers.isAsyncUtil(node)) { + return context.report({ + node, + messageId: 'asyncUtilError', + data: { utilName: node.name }, + }); + } + + if (helpers.isUserEventMethod(node)) { + return context.report({ node, messageId: 'userEventError' }); + } + + // force queries to be reported + if (helpers.isCustomQuery(node)) { + return context.report({ node, messageId: 'customQueryError' }); + } + + if (helpers.isGetQueryVariant(node)) { + return context.report({ node, messageId: 'getByError' }); + } + + if (helpers.isQueryQueryVariant(node)) { + return context.report({ node, messageId: 'queryByError' }); + } + + if (helpers.isFindQueryVariant(node)) { + return context.report({ node, messageId: 'findByError' }); + } + + return undefined; + }; + + const reportMemberExpression = (node: TSESTree.MemberExpression) => { + if (helpers.isPresenceAssert(node)) { + return context.report({ node, messageId: 'presenceAssertError' }); + } + + if (helpers.isAbsenceAssert(node)) { + return context.report({ node, messageId: 'absenceAssertError' }); + } + + return undefined; + }; + + const reportImportDeclaration = (node: TSESTree.ImportDeclaration) => { + // This is just to check that defining an `ImportDeclaration` doesn't + // override `ImportDeclaration` from `detectTestingLibraryUtils` + if (node.source.value === 'report-me') { + context.report({ node, messageId: 'fakeError' }); + } + }; + + return { + 'CallExpression Identifier': reportCallExpressionIdentifier, + MemberExpression: reportMemberExpression, + ImportDeclaration: reportImportDeclaration, + 'Program:exit'() { + const importNode = helpers.getCustomModuleImportNode(); + const importName = helpers.getCustomModuleImportName(); + if (!importNode) { + return; + } + + if (importName === 'custom-module-forced-report') { + context.report({ + node: importNode, + messageId: 'fakeError', + }); + } + }, + }; + }, }); diff --git a/tests/index.test.ts b/tests/index.test.ts index 754152a5..0d3ab20d 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,76 +1,80 @@ -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 = 26; +const numberOfRules = 28; const ruleNames = Object.keys(plugin.rules); // eslint-disable-next-line jest/expect-expect it('should have a corresponding doc for each rule', () => { - ruleNames.forEach((rule) => { - const docPath = resolve(__dirname, '../docs/rules', `${rule}.md`); - - if (!existsSync(docPath)) { - throw new Error( - `Could not find documentation file for rule "${rule}" in path "${docPath}"` - ); - } - }); + ruleNames.forEach((rule) => { + const docPath = resolve(__dirname, '../docs/rules', `${rule}.md`); + + if (!existsSync(docPath)) { + throw new Error( + `Could not find documentation file for rule "${rule}" in path "${docPath}"` + ); + } + }); }); // eslint-disable-next-line jest/expect-expect it('should have a corresponding test for each rule', () => { - ruleNames.forEach((rule) => { - const testPath = resolve(__dirname, './lib/rules/', `${rule}.test.ts`); - - if (!existsSync(testPath)) { - throw new Error( - `Could not find test file for rule "${rule}" in path "${testPath}"` - ); - } - }); + ruleNames.forEach((rule) => { + const testPath = resolve(__dirname, './lib/rules/', `${rule}.test.ts`); + + if (!existsSync(testPath)) { + throw new Error( + `Could not find test file for rule "${rule}" in path "${testPath}"` + ); + } + }); }); // eslint-disable-next-line jest/expect-expect it('should have the correct amount of rules', () => { - const { length } = ruleNames; - - if (length !== numberOfRules) { - throw new Error( - `There should be exactly ${numberOfRules} rules, but there are ${length}. If you've added a new rule, please update this number.` - ); - } -}); + const { length } = ruleNames; -it("should have run 'generate:configs' script when changing config rules", async () => { - generateConfigs(); - - const allConfigs = plugin.configs; - expect(allConfigs).toMatchSnapshot(); + if (length !== numberOfRules) { + throw new Error( + `There should be exactly ${numberOfRules} rules, but there are ${length}. If you've added a new rule, please update this number.` + ); + } }); it('should export configs that refer to actual rules', () => { - const allConfigs = plugin.configs; - - expect(Object.keys(allConfigs)).toEqual(['dom', 'angular', 'react', 'vue']); - const allConfigRules = Object.values(allConfigs) - .map((config) => Object.keys(config.rules)) - .reduce((previousValue, currentValue) => [ - ...previousValue, - ...currentValue, - ]); - - allConfigRules.forEach((rule) => { - const ruleNamePrefix = 'testing-library/'; - const ruleName = rule.slice(ruleNamePrefix.length); - - expect(rule.startsWith(ruleNamePrefix)).toBe(true); - expect(ruleNames).toContain(ruleName); - - expect(() => require(`../lib/rules/${ruleName}`)).not.toThrow(); - }); + const allConfigs = plugin.configs; + + expect(Object.keys(allConfigs)).toEqual([ + 'dom', + 'angular', + 'react', + 'vue', + 'svelte', + 'marko', + 'flat/dom', + 'flat/angular', + 'flat/react', + 'flat/vue', + 'flat/svelte', + 'flat/marko', + ]); + const allConfigRules = Object.values(allConfigs) + .map((config) => Object.keys(config.rules ?? {})) + .reduce((previousValue, currentValue) => [ + ...previousValue, + ...currentValue, + ]); + + allConfigRules.forEach((rule) => { + const ruleNamePrefix = 'testing-library/'; + const ruleName = rule.slice(ruleNamePrefix.length); + + expect(rule.startsWith(ruleNamePrefix)).toBe(true); + expect(ruleNames).toContain(ruleName); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-require-imports + expect(() => require(`../lib/rules/${ruleName}`)).not.toThrow(); + }); }); diff --git a/tests/lib/rules/await-async-events.test.ts b/tests/lib/rules/await-async-events.test.ts new file mode 100644 index 00000000..01de4d16 --- /dev/null +++ b/tests/lib/rules/await-async-events.test.ts @@ -0,0 +1,1253 @@ +import rule, { + Options, + RULE_NAME, +} from '../../../lib/rules/await-async-events'; +import { createRuleTester } from '../test-utils'; + +const ruleTester = createRuleTester(); + +const FIRE_EVENT_ASYNC_FUNCTIONS = [ + 'click', + 'change', + 'focus', + 'blur', + 'keyDown', +] as const; +const USER_EVENT_ASYNC_FUNCTIONS = [ + 'click', + 'dblClick', + 'tripleClick', + 'hover', + 'unhover', + 'tab', + 'keyboard', + 'copy', + 'cut', + 'paste', + 'pointer', + 'clear', + 'deselectOptions', + 'selectOptions', + 'type', + 'upload', +] as const; +const FIRE_EVENT_ASYNC_FRAMEWORKS = [ + '@testing-library/vue', + '@marko/testing-library', +] as const; +const USER_EVENT_ASYNC_FRAMEWORKS = ['@testing-library/user-event'] as const; + +ruleTester.run(RULE_NAME, rule, { + valid: [ + ...FIRE_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [ + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('event method not called is valid', () => { + fireEvent.${eventMethod} + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('await promise from event method is valid', async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('await several promises from event methods is valid', async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('await promise kept in a var from event method is valid', async () => { + const promise = fireEvent.${eventMethod}(getByLabelText('username')) + await promise + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('chain then method to promise from event method is valid', async (done) => { + fireEvent.${eventMethod}(getByLabelText('username')) + .then(() => { done() }) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('chain then method to several promises from event methods is valid', async (done) => { + fireEvent.${eventMethod}(getByLabelText('username')).then(() => { + fireEvent.${eventMethod}(getByLabelText('username')).then(() => { done() }) + }) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + { + code: ` + import { fireEvent } from '${testingFramework}' + test('event methods wrapped with Promise.all are valid', async () => { + await Promise.all([ + fireEvent.${FIRE_EVENT_ASYNC_FUNCTIONS[0]}(getByText('Click me')), + fireEvent.${FIRE_EVENT_ASYNC_FUNCTIONS[1]}(getByText('Click me')), + ]) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + }, + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('return promise from event methods is valid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('await promise returned from function wrapping event method is valid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + await triggerEvent() + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('await promise assigned to a variable from function wrapping event method is valid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + const result = await triggerEvent() + expect(result).toBe(undefined) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { fireEvent } from 'somewhere-else' // not using ${testingFramework} + test('unhandled promise from event not related to TL is valid', async () => { + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { fireEvent } from 'test-utils' // implicitly using ${testingFramework} + test('await promise from event method imported from custom module is valid', async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + { + // edge case for coverage: + // valid use case without call expression + // so there is no innermost function scope found + code: ` + import { fireEvent } from 'test-utils' // implicitly using ${testingFramework} + test('edge case for innermost function without call expression', async () => { + function triggerEvent() { + doSomething() + return fireEvent.focus(getByLabelText('username')) + } + + const reassignedFunction = triggerEvent + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + }, + + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('jest async matchers are valid', async () => { + expect(fireEvent.${eventMethod}(getByLabelText('username'))).rejects.toBe("foo") + expect(fireEvent.${eventMethod}(getByLabelText('username'))).resolves.toBe("foo") + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('jest-extended async matchers are valid', async () => { + expect(fireEvent.${eventMethod}(getByLabelText('username'))).toReject() + expect(fireEvent.${eventMethod}(getByLabelText('username'))).toResolve() + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('jasmine async matchers are valid', async () => { + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeRejected() + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeRejectedWith("foo") + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeRejectedWithError("foo") + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBePending() + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeResolved() + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeResolvedTo("foo") + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ]), + + ...USER_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import userEvent from '${testingFramework}' + test('setup method called is valid', () => { + userEvent.setup() + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + }, + { + code: ` + import userEvent from '${testingFramework}' + function customSetup() { + return { + user: userEvent.setup() + }; + } + test('setup method called and returned is valid', () => { + const {user} = customSetup(); + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + }, + { + code: ` + import userEvent from '${testingFramework}' + const customSetup = () => userEvent.setup(); + test('setup method called and returned as arrow function body is valid', () => { + const user = customSetup(); + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + }, + { + code: ` + import userEvent from '${testingFramework}' + function customSetup() { + const user = userEvent.setup(); + return { user }; + } + test('setup method called and returned is valid', () => { + const { user } = customSetup(); + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + }, + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('event method not called is valid', () => { + userEvent.${eventMethod} + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await promise from event method is valid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await several promises from event methods is valid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await promise kept in a var from event method is valid', async () => { + const promise = userEvent.${eventMethod}(getByLabelText('username')) + await promise + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('chain then method to promise from event method is valid', async (done) => { + userEvent.${eventMethod}(getByLabelText('username')) + .then(() => { done() }) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('chain then method to several promises from event methods is valid', async (done) => { + userEvent.${eventMethod}(getByLabelText('username')).then(() => { + userEvent.${eventMethod}(getByLabelText('username')).then(() => { done() }) + }) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + { + code: ` + import userEvent from '${testingFramework}' + test('event methods wrapped with Promise.all are valid', async () => { + await Promise.all([ + userEvent.${USER_EVENT_ASYNC_FUNCTIONS[0]}(getByText('Click me')), + userEvent.${USER_EVENT_ASYNC_FUNCTIONS[1]}(getByText('Click me')), + ]) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + }, + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('return promise from event methods is valid', () => { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await promise returned from function wrapping event method is valid', () => { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + await triggerEvent() + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await promise assigned to a variable from function wrapping event method is valid', () => { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + const result = await triggerEvent() + expect(result).toBe(undefined) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await expression that evaluates to promise is valid', async () => { + await (null, userEvent.${eventMethod}(getByLabelText('username'))); + await (condition ? null : userEvent.${eventMethod}(getByLabelText('username'))); + await (condition && userEvent.${eventMethod}(getByLabelText('username'))); + await (userEvent.${eventMethod}(getByLabelText('username')) || userEvent.${eventMethod}(getByLabelText('username'))); + await (userEvent.${eventMethod}(getByLabelText('username')) ?? userEvent.${eventMethod}(getByLabelText('username'))); + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import userEvent from 'somewhere-else' + test('unhandled promise from event not related to TL is valid', async () => { + userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import userEvent from 'test-utils' + test('await promise from event method imported from custom module is valid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await promise from userEvent relying on default options', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + })), + { + // edge case for coverage: + // valid use case without call expression + // so there is no innermost function scope found + code: ` + import userEvent from 'test-utils' + test('edge case for innermost function without call expression', async () => { + function triggerEvent() { + doSomething() + return userEvent.focus(getByLabelText('username')) + } + + const reassignedFunction = triggerEvent + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + }, + { + code: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('await promises from multiple event modules', async () => { + await fireEvent.click(getByLabelText('username')) + await userEvent.click(getByLabelText('username')) + }) + `, + options: [{ eventModule: ['userEvent', 'fireEvent'] }] as Options, + }, + ]), + ], + + invalid: [ + ...FIRE_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [ + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise from event method is invalid', () => { + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 19 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise from event method is invalid', async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + + fireEvent.${eventMethod}(getByLabelText('username')) + `, + errors: [ + { + line: 4, + column: 7, + endColumn: 17 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: null, + }) as const + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + + function run() { + fireEvent.${eventMethod}(getByLabelText('username')) + } + + test('should handle external function', run) + `, + errors: [ + { + line: 5, + column: 9, + endColumn: 19 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + + async function run() { + await fireEvent.${eventMethod}(getByLabelText('username')) + } + + test('should handle external function', run) + `, + }) as const + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent as testingLibraryFireEvent } from '${testingFramework}' + test('unhandled promise from aliased event method is invalid', async () => { + testingLibraryFireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 33 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent as testingLibraryFireEvent } from '${testingFramework}' + test('unhandled promise from aliased event method is invalid', async () => { + await testingLibraryFireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import * as testingLibrary from '${testingFramework}' + test('unhandled promise from wildcard imported event method is invalid', async () => { + testingLibrary.fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 34 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import * as testingLibrary from '${testingFramework}' + test('unhandled promise from wildcard imported event method is invalid', async () => { + await testingLibrary.fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('several unhandled promises from event methods is invalid', async function() { + fireEvent.${eventMethod}(getByLabelText('username')) + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + { + line: 5, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('several unhandled promises from event methods is invalid', async function() { + await fireEvent.${eventMethod}(getByLabelText('username')) + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise from event method with aggressive reporting opted-out is invalid', function() { + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise from event method with aggressive reporting opted-out is invalid', async function() { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { fireEvent } from 'test-utils' // implicitly using ${testingFramework} + test( + 'unhandled promise from event method imported from custom module with aggressive reporting opted-out is invalid', + () => { + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 6, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from 'test-utils' // implicitly using ${testingFramework} + test( + 'unhandled promise from event method imported from custom module with aggressive reporting opted-out is invalid', + async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { fireEvent } from '${testingFramework}' + test( + 'unhandled promise from event method imported from default module with aggressive reporting opted-out is invalid', + () => { + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 6, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test( + 'unhandled promise from event method imported from default module with aggressive reporting opted-out is invalid', + async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + test( + 'unhandled promise from event method kept in a var is invalid', + () => { + const promise = fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 6, + column: 25, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test( + 'unhandled promise from event method kept in a var is invalid', + async () => { + const promise = await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise returned from function wrapping event method is invalid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + triggerEvent() + }) + `, + errors: [ + { + line: 9, + column: 9, + messageId: 'awaitAsyncEventWrapper', + data: { name: 'triggerEvent' }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise returned from function wrapping event method is invalid', async () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + await triggerEvent() + }) + `, + }) as const + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise assigned to a variable returned from function wrapping event method is invalid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + const result = triggerEvent() + expect(result).toBe(undefined) + }) + `, + errors: [ + { + line: 9, + column: 24, + messageId: 'awaitAsyncEventWrapper', + data: { name: 'triggerEvent' }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise assigned to a variable returned from function wrapping event method is invalid', async () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + const result = await triggerEvent() + expect(result).toBe(undefined) + }) + `, + }) as const + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + triggerEvent() + `, + errors: [ + { + line: 9, + column: 7, + messageId: 'awaitAsyncEventWrapper', + data: { name: 'triggerEvent' }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: null, + }) as const + ), + ]), + ...USER_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [ + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test('unhandled promise from event method is invalid', () => { + userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 19 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('unhandled promise from event method is invalid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + + userEvent.${eventMethod}(getByLabelText('username')) + `, + errors: [ + { + line: 4, + column: 7, + endColumn: 17 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: null, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import testingLibraryUserEvent from '${testingFramework}' + test('unhandled promise imported from alternate name event method is invalid', () => { + testingLibraryUserEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 33 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import testingLibraryUserEvent from '${testingFramework}' + test('unhandled promise imported from alternate name event method is invalid', async () => { + await testingLibraryUserEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test('several unhandled promises from event methods is invalid', () => { + userEvent.${eventMethod}(getByLabelText('username')) + userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + { + line: 5, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('several unhandled promises from event methods is invalid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test( + 'unhandled promise from event method kept in a var is invalid', + () => { + const promise = userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 6, + column: 25, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test( + 'unhandled promise from event method kept in a var is invalid', + async () => { + const promise = await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test('unhandled promise returned from function wrapping event method is invalid', function() { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + triggerEvent() + }) + `, + errors: [ + { + line: 9, + column: 9, + messageId: 'awaitAsyncEventWrapper', + data: { name: 'triggerEvent' }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('unhandled promise returned from function wrapping event method is invalid', async function() { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + await triggerEvent() + }) + `, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test('unhandled promise assigned to a variable returned from function wrapping event method is invalid', function() { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + const result = triggerEvent() + expect(result).toBe(undefined) + }) + `, + errors: [ + { + line: 9, + column: 24, + messageId: 'awaitAsyncEventWrapper', + data: { name: 'triggerEvent' }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('unhandled promise assigned to a variable returned from function wrapping event method is invalid', async function() { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + const result = await triggerEvent() + expect(result).toBe(undefined) + }) + `, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + triggerEvent() + `, + errors: [ + { + line: 9, + column: 7, + messageId: 'awaitAsyncEventWrapper', + data: { name: 'triggerEvent' }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: null, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test('unhandled expression that evaluates to promise is invalid', () => { + condition ? null : (null, true && userEvent.${eventMethod}(getByLabelText('username'))); + }); + `, + errors: [ + { + line: 4, + column: 38, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('unhandled expression that evaluates to promise is invalid', async () => { + condition ? null : (null, true && await userEvent.${eventMethod}(getByLabelText('username'))); + }); + `, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test('handled AND expression with left promise is invalid', async () => { + await (userEvent.${eventMethod}(getByLabelText('username')) && userEvent.${eventMethod}(getByLabelText('username'))); + }); + `, + errors: [ + { + line: 4, + column: 11, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('handled AND expression with left promise is invalid', async () => { + await (await userEvent.${eventMethod}(getByLabelText('username')) && userEvent.${eventMethod}(getByLabelText('username'))); + }); + `, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test('voided promise is invalid', async () => { + await void userEvent.${eventMethod}(getByLabelText('username')); + await (userEvent.${eventMethod}(getByLabelText('username')), null); + }); + `, + errors: [ + { + line: 4, + column: 15, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + { + line: 5, + column: 11, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('voided promise is invalid', async () => { + await void await userEvent.${eventMethod}(getByLabelText('username')); + await (await userEvent.${eventMethod}(getByLabelText('username')), null); + }); + `, + }) as const + ), + ]), + { + code: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('unhandled promises from multiple event modules', () => { + fireEvent.click(getByLabelText('username')) + userEvent.click(getByLabelText('username')) + }) + `, + errors: [ + { + line: 5, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: 'click' }, + }, + { + line: 6, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: 'click' }, + }, + ], + options: [{ eventModule: ['userEvent', 'fireEvent'] }] as Options, + output: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('unhandled promises from multiple event modules', async () => { + await fireEvent.click(getByLabelText('username')) + await userEvent.click(getByLabelText('username')) + }) + `, + }, + { + code: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('unhandled promise from userEvent relying on default options', async function() { + fireEvent.click(getByLabelText('username')) + userEvent.click(getByLabelText('username')) + }) + `, + errors: [ + { + line: 6, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: 'click' }, + }, + ], + output: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('unhandled promise from userEvent relying on default options', async function() { + fireEvent.click(getByLabelText('username')) + await userEvent.click(getByLabelText('username')) + }) + `, + }, + ], +}); diff --git a/tests/lib/rules/await-async-queries.test.ts b/tests/lib/rules/await-async-queries.test.ts new file mode 100644 index 00000000..b661d2cb --- /dev/null +++ b/tests/lib/rules/await-async-queries.test.ts @@ -0,0 +1,605 @@ +import { TSESLint } from '@typescript-eslint/utils'; + +import rule, { RULE_NAME } from '../../../lib/rules/await-async-queries'; +import { + ASYNC_QUERIES_COMBINATIONS, + ASYNC_QUERIES_VARIANTS, + combineQueries, + SYNC_QUERIES_COMBINATIONS, +} from '../../../lib/utils'; +import { createRuleTester } from '../test-utils'; + +const ruleTester = createRuleTester(); + +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + +interface TestCode { + code: string; + isAsync?: boolean; + testingFramework: string; +} + +function createTestCode({ + code, + isAsync = true, + testingFramework = '@testing-library/react', +}: TestCode) { + return ` + import { render } from '${testingFramework}' + test("An example test",${isAsync ? ' async ' : ' '}() => { + ${code} + }) + `; +} + +interface TestCaseParams { + isAsync?: boolean; + combinations?: string[]; + errors?: TSESLint.TestCaseError<'asyncQueryWrapper' | 'awaitAsyncQuery'>[]; + testingFramework?: string; +} + +function createTestCase( + getTest: ( + query: string + ) => + | string + | { code: string; errors?: TSESLint.TestCaseError<'awaitAsyncQuery'>[] }, + { + combinations = ALL_ASYNC_COMBINATIONS_TO_TEST, + isAsync, + testingFramework = '', + }: TestCaseParams = {} +) { + return combinations.map((query) => { + const test = getTest(query); + + return typeof test === 'string' + ? { + code: createTestCode({ code: test, isAsync, testingFramework }), + errors: [], + } + : { + code: createTestCode({ code: test.code, isAsync, testingFramework }), + errors: test.errors, + }; + }); +} + +const CUSTOM_ASYNC_QUERIES_COMBINATIONS = combineQueries( + ASYNC_QUERIES_VARIANTS, + ['ByIcon', 'ByButton'] +); + +// built-in queries + custom queries +const ALL_ASYNC_COMBINATIONS_TO_TEST = [ + ...ASYNC_QUERIES_COMBINATIONS, + ...CUSTOM_ASYNC_QUERIES_COMBINATIONS, +]; + +ruleTester.run(RULE_NAME, rule, { + valid: [ + // async queries declaration from render functions are valid + ...createTestCase((query) => `const { ${query} } = render()`, { + isAsync: false, + }), + + // async screen queries declaration are valid + ...createTestCase((query) => `await screen.${query}('foo')`), + + // async queries with optional chaining are valid + ...createTestCase((query) => `await screen?.${query}('foo')`), + + ...createTestCase( + (query) => ` + it('test case', async () => { + let renderResult: RenderResult | undefined; + + renderResult = render(
text
); + + expect(await renderResult?.${query}('text')).toBeDefined(); + }); + ` + ), + + // async @marko/testing-library screen queries declaration are valid + ...createTestCase((query) => `await screen.${query}('foo')`, { + testingFramework: '@marko/testing-library', + }), + + // async queries not called are valid + ...createTestCase((query) => `expect(screen.${query}).toBeDefined()`, { + isAsync: false, + }), + + // async queries are valid with await operator + ...createTestCase( + (query) => ` + doSomething() + await ${query}('foo') + ` + ), + + // async queries are valid when saved in a variable with await operator + ...createTestCase( + (query) => ` + doSomething() + const foo = await ${query}('foo') + expect(foo).toBeInTheDocument(); + ` + ), + + // async queries are valid when saved in a promise variable immediately resolved + ...createTestCase( + (query) => ` + const promise = ${query}('foo') + await promise + ` + ), + + // async queries are valid when used with then method + ...createTestCase( + (query) => ` + ${query}('foo').then(() => { + done() + }) + ` + ), + + // async queries are valid with promise in variable resolved by then method + ...createTestCase( + (query) => ` + const promise = ${query}('foo') + promise.then((done) => done()) + ` + ), + + // async queries are valid when wrapped within Promise.all + await expression + ...createTestCase( + (query) => ` + doSomething() + + await Promise.all([ + ${query}('foo'), + ${query}('bar'), + ]); + ` + ), + + // async queries are valid when wrapped within Promise.all + then chained + ...createTestCase( + (query) => ` + doSomething() + + Promise.all([ + ${query}('foo'), + ${query}('bar'), + ]).then() + ` + ), + + // async queries are valid when wrapped within Promise.allSettled + await expression + ...createTestCase( + (query) => ` + doSomething() + + await Promise.allSettled([ + ${query}('foo'), + ${query}('bar'), + ]); + ` + ), + + // async queries are valid when wrapped within Promise.allSettled + then chained + ...createTestCase( + (query) => ` + doSomething() + + Promise.allSettled([ + ${query}('foo'), + ${query}('bar'), + ]).then() + ` + ), + + // async queries are valid with promise returned in arrow function + ...createTestCase( + (query) => `const anArrowFunction = () => ${query}('foo')` + ), + + // async queries are valid with promise returned in regular function + ...createTestCase((query) => `function foo() { return ${query}('foo') }`), + + // async queries are valid with promise in variable and returned in regular function + ...createTestCase( + (query) => ` + async function queryWrapper() { + const promise = ${query}('foo') + return promise + } + ` + ), + + // sync queries are valid + ...createTestCase( + (query) => ` + doSomething() + ${query}('foo') + `, + { combinations: SYNC_QUERIES_COMBINATIONS } + ), + + // async queries with resolves matchers are valid + ...createTestCase( + (query) => ` + expect(${query}("foo")).resolves.toBe("bar") + expect(wrappedQuery(${query}("foo"))).resolves.toBe("bar") + ` + ), + // async queries with toResolve matchers are valid + ...createTestCase( + (query) => ` + expect(${query}("foo")).toResolve() + expect(wrappedQuery(${query}("foo"))).toResolve() + ` + ), + + // async queries with rejects matchers are valid + ...createTestCase( + (query) => ` + expect(${query}("foo")).rejects.toBe("bar") + expect(wrappedQuery(${query}("foo"))).rejects.toBe("bar") + ` + ), + + // async queries with toReject matchers are valid + ...createTestCase( + (query) => ` + expect(${query}("foo")).toReject() + expect(wrappedQuery(${query}("foo"))).toReject() + ` + ), + + // jasmine async matchers are valid + ...createTestCase( + (query) => ` + expectAsync(${query}("foo")).toBeRejected() + expectAsync(wrappedQuery(${query}("foo"))).toBeRejected() + expectAsync(${query}("foo")).toBeRejectedWith("bar") + expectAsync(wrappedQuery(${query}("foo"))).toBeRejectedWith("bar") + expectAsync(${query}("foo")).toBeRejectedWithError("bar") + expectAsync(wrappedQuery(${query}("foo"))).toBeRejectedWithError("bar") + expectAsync(${query}("foo")).toBePending() + expectAsync(wrappedQuery(${query}("foo"))).toBePending() + expectAsync(${query}("foo")).toBeResolved() + expectAsync(wrappedQuery(${query}("foo"))).toBeResolved() + expectAsync(${query}("foo")).toBeResolvedTo("bar") + expectAsync(wrappedQuery(${query}("foo"))).toBeResolvedTo("bar") + ` + ), + + // unresolved async queries with aggressive reporting opted-out are valid + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from "another-library" + + test('An example test', async () => { + const example = ${query}("my example") + }) + `, + })), + + // handled promise assigned to variable returned from async query wrapper is valid + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` + const queryWrapper = () => { + return screen.${query}('foo') + } + + test("A valid example test", async () => { + const element = await queryWrapper() + expect(element).toBeVisible() + }) + `, + }) as const + ), + + // non-matching query is valid + ` + test('A valid example test', async () => { + const example = findText("my example") + }) + `, + + // unhandled promise from non-matching query is valid + ` + async function findButton() { + const element = findByText('outer element') + return somethingElse(element) + } + + test('A valid example test', async () => { + // findButton doesn't match async query pattern + const button = findButton() + }) + `, + + // unhandled promise from custom query not matching custom-queries setting is valid + { + settings: { + 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'], + }, + code: ` + test('A valid example test', () => { + const element = findByIcon('search') + }) + `, + }, + + // unhandled promise from custom query with aggressive query switched off is valid + { + settings: { + 'testing-library/custom-queries': 'off', + }, + code: ` + test('A valid example test', () => { + const element = findByIcon('search') + }) + `, + }, + + // edge case for coverage + // return non-matching query and other than Identifier or CallExpression + ` + async function someSetup() { + const element = await findByText('outer element') + return element ? findSomethingElse(element) : null + } + + test('A valid example test', async () => { + someSetup() + }) + `, + + // https://github.com/testing-library/eslint-plugin-testing-library/issues/359 + `// issue #359 + import { render, screen } from 'mocks/test-utils' + import userEvent from '@testing-library/user-event' + + const testData = { + name: 'John Doe', + email: 'john@doe.com', + password: 'extremeSecret', + } + + const selectors = { + username: () => screen.findByRole('textbox', { name: /username/i }), + email: () => screen.findByRole('textbox', { name: /e-mail/i }), + password: () => screen.findByLabelText(/password/i), + } + + test('this is a valid case', async () => { + render() + userEvent.type(await selectors.username(), testData.name) + userEvent.type(await selectors.email(), testData.email) + userEvent.type(await selectors.password(), testData.password) + // ... + }) + `, + + // edge case for coverage + // valid async query usage without any function defined + // so there is no innermost function scope found + `const element = await findByRole('button')`, + ], + + invalid: [ + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => + ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: `// async queries without await operator or then method are not valid + import { render } from '${testingFramework}' + + test("An example test", async () => { + doSomething() + const foo = ${query}('foo') + }); + `, + errors: [{ messageId: 'awaitAsyncQuery', line: 6, column: 21 }], + }) as const + ) + ), + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: `// async screen queries without await operator or then method are not valid + import { render } from '@testing-library/react' + + test("An example test", async () => { + screen.${query}('foo') + }); + `, + errors: [ + { + messageId: 'awaitAsyncQuery', + line: 5, + column: 16, + data: { name: query }, + }, + ], + }) as const + ), + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` + import { render } from '@testing-library/react' + + test("An example test", async () => { + doSomething() + const foo = ${query}('foo') + }); + `, + errors: [ + { + messageId: 'awaitAsyncQuery', + line: 6, + column: 21, + data: { name: query }, + }, + ], + }) as const + ), + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` + import { render } from '@testing-library/react' + + test("An example test", async () => { + const foo = ${query}('foo') + expect(foo).toBeInTheDocument() + expect(foo).toHaveAttribute('src', 'bar'); + }); + `, + errors: [ + { + messageId: 'awaitAsyncQuery', + line: 5, + column: 21, + data: { name: query }, + }, + ], + }) as const + ), + + // unresolved async queries are not valid (aggressive reporting) + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` + import { render } from "another-library" + + test('An example test', async () => { + const example = ${query}("my example") + }) + `, + errors: [{ messageId: 'awaitAsyncQuery', line: 5, column: 27 }], + }) as const + ), + + // unhandled promise from async query function wrapper is invalid + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` + function queryWrapper() { + doSomethingElse(); + + return screen.${query}('foo') + } + + test("An invalid example test", () => { + const element = queryWrapper() + }) + + test("An invalid example test", async () => { + const element = await queryWrapper() + }) + `, + errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], + }) as const + ), + // unhandled promise from async query arrow function wrapper is invalid + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` + const queryWrapper = () => { + doSomethingElse(); + + return ${query}('foo') + } + + test("An invalid example test", () => { + const element = queryWrapper() + }) + + test("An invalid example test", async () => { + const element = await queryWrapper() + }) + `, + errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], + }) as const + ), + // unhandled promise implicitly returned from async query arrow function wrapper is invalid + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` + const queryWrapper = () => screen.${query}('foo') + + test("An invalid example test", () => { + const element = queryWrapper() + }) + + test("An invalid example test", async () => { + const element = await queryWrapper() + }) + `, + errors: [{ messageId: 'asyncQueryWrapper', line: 5, column: 27 }], + }) as const + ), + + // unhandled promise from custom query matching custom-queries setting is invalid + { + settings: { + 'testing-library/custom-queries': ['ByIcon', 'getByComplexText'], + }, + code: ` + test('An invalid example test', () => { + const element = findByIcon('search') + }) + `, + errors: [{ messageId: 'awaitAsyncQuery', line: 3, column: 25 }], + }, + + { + code: `// similar to issue #359 but forcing an error in no-awaited wrapper + import { render, screen } from 'mocks/test-utils' + import userEvent from '@testing-library/user-event' + + const testData = { + name: 'John Doe', + email: 'john@doe.com', + password: 'extremeSecret', + } + + const selectors = { + username: () => screen.findByRole('textbox', { name: /username/i }), + email: () => screen.findByRole('textbox', { name: /e-mail/i }), + password: () => screen.findByLabelText(/password/i), + } + + test('this is a valid case', async () => { + render() + userEvent.type(selectors.username(), testData.name) // <-- unhandled here + userEvent.type(await selectors.email(), testData.email) + userEvent.type(await selectors.password(), testData.password) + // ... + }) + `, + errors: [{ messageId: 'asyncQueryWrapper', line: 19, column: 34 }], + }, + ], +}); diff --git a/tests/lib/rules/await-async-query.test.ts b/tests/lib/rules/await-async-query.test.ts deleted file mode 100644 index 648b0ce5..00000000 --- a/tests/lib/rules/await-async-query.test.ts +++ /dev/null @@ -1,510 +0,0 @@ -import { TSESLint } from '@typescript-eslint/experimental-utils'; - -import rule, { RULE_NAME } from '../../../lib/rules/await-async-query'; -import { - ASYNC_QUERIES_COMBINATIONS, - ASYNC_QUERIES_VARIANTS, - combineQueries, - SYNC_QUERIES_COMBINATIONS, -} from '../../../lib/utils'; -import { createRuleTester } from '../test-utils'; - -const ruleTester = createRuleTester(); - -interface TestCode { - code: string; - isAsync?: boolean; -} - -function createTestCode({ code, isAsync = true }: TestCode) { - return ` - import { render } from '@testing-library/react' - test("An example test",${isAsync ? ' async ' : ' '}() => { - ${code} - }) - `; -} - -interface TestCaseParams { - isAsync?: boolean; - combinations?: string[]; - errors?: TSESLint.TestCaseError<'asyncQueryWrapper' | 'awaitAsyncQuery'>[]; -} - -function createTestCase( - getTest: ( - query: string - ) => - | string - | { code: string; errors?: TSESLint.TestCaseError<'awaitAsyncQuery'>[] }, - { - combinations = ALL_ASYNC_COMBINATIONS_TO_TEST, - isAsync, - }: TestCaseParams = {} -) { - return combinations.map((query) => { - const test = getTest(query); - - return typeof test === 'string' - ? { code: createTestCode({ code: test, isAsync }), errors: [] } - : { - code: createTestCode({ code: test.code, isAsync }), - errors: test.errors, - }; - }); -} - -const CUSTOM_ASYNC_QUERIES_COMBINATIONS = combineQueries( - ASYNC_QUERIES_VARIANTS, - ['ByIcon', 'ByButton'] -); - -// built-in queries + custom queries -const ALL_ASYNC_COMBINATIONS_TO_TEST = [ - ...ASYNC_QUERIES_COMBINATIONS, - ...CUSTOM_ASYNC_QUERIES_COMBINATIONS, -]; - -ruleTester.run(RULE_NAME, rule, { - valid: [ - // async queries declaration from render functions are valid - ...createTestCase((query) => `const { ${query} } = render()`, { - isAsync: false, - }), - - // async screen queries declaration are valid - ...createTestCase((query) => `await screen.${query}('foo')`), - - // async queries are valid with await operator - ...createTestCase( - (query) => ` - doSomething() - await ${query}('foo') - ` - ), - - // async queries are valid when saved in a variable with await operator - ...createTestCase( - (query) => ` - doSomething() - const foo = await ${query}('foo') - expect(foo).toBeInTheDocument(); - ` - ), - - // async queries are valid when saved in a promise variable immediately resolved - ...createTestCase( - (query) => ` - const promise = ${query}('foo') - await promise - ` - ), - - // async queries are valid when used with then method - ...createTestCase( - (query) => ` - ${query}('foo').then(() => { - done() - }) - ` - ), - - // async queries are valid with promise in variable resolved by then method - ...createTestCase( - (query) => ` - const promise = ${query}('foo') - promise.then((done) => done()) - ` - ), - - // async queries are valid when wrapped within Promise.all + await expression - ...createTestCase( - (query) => ` - doSomething() - - await Promise.all([ - ${query}('foo'), - ${query}('bar'), - ]); - ` - ), - - // async queries are valid when wrapped within Promise.all + then chained - ...createTestCase( - (query) => ` - doSomething() - - Promise.all([ - ${query}('foo'), - ${query}('bar'), - ]).then() - ` - ), - - // async queries are valid when wrapped within Promise.allSettled + await expression - ...createTestCase( - (query) => ` - doSomething() - - await Promise.allSettled([ - ${query}('foo'), - ${query}('bar'), - ]); - ` - ), - - // async queries are valid when wrapped within Promise.allSettled + then chained - ...createTestCase( - (query) => ` - doSomething() - - Promise.allSettled([ - ${query}('foo'), - ${query}('bar'), - ]).then() - ` - ), - - // async queries are valid with promise returned in arrow function - ...createTestCase( - (query) => `const anArrowFunction = () => ${query}('foo')` - ), - - // async queries are valid with promise returned in regular function - ...createTestCase((query) => `function foo() { return ${query}('foo') }`), - - // async queries are valid with promise in variable and returned in regular function - ...createTestCase( - (query) => ` - async function queryWrapper() { - const promise = ${query}('foo') - return promise - } - ` - ), - - // sync queries are valid - ...createTestCase( - (query) => ` - doSomething() - ${query}('foo') - `, - { combinations: SYNC_QUERIES_COMBINATIONS } - ), - - // async queries with resolves matchers are valid - ...createTestCase( - (query) => ` - expect(${query}("foo")).resolves.toBe("bar") - expect(wrappedQuery(${query}("foo"))).resolves.toBe("bar") - ` - ), - - // async queries with rejects matchers are valid - ...createTestCase( - (query) => ` - expect(${query}("foo")).rejects.toBe("bar") - expect(wrappedQuery(${query}("foo"))).rejects.toBe("bar") - ` - ), - - // unresolved async queries with aggressive reporting opted-out are valid - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from "another-library" - - test('An example test', async () => { - const example = ${query}("my example") - }) - `, - })), - - // non-matching query is valid - ` - test('A valid example test', async () => { - const example = findText("my example") - }) - `, - - // unhandled promise from non-matching query is valid - ` - async function findButton() { - const element = findByText('outer element') - return somethingElse(element) - } - - test('A valid example test', async () => { - // findButton doesn't match async query pattern - const button = findButton() - }) - `, - - // unhandled promise from custom query not matching custom-queries setting is valid - { - settings: { - 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'], - }, - code: ` - test('A valid example test', () => { - const element = findByIcon('search') - }) - `, - }, - - // unhandled promise from custom query with aggressive query switched off is valid - { - settings: { - 'testing-library/custom-queries': 'off', - }, - code: ` - test('A valid example test', () => { - const element = findByIcon('search') - }) - `, - }, - - // edge case for coverage - // return non-matching query and other than Identifier or CallExpression - ` - async function someSetup() { - const element = await findByText('outer element') - return element ? findSomethingElse(element) : null - } - - test('A valid example test', async () => { - someSetup() - }) - `, - - // https://github.com/testing-library/eslint-plugin-testing-library/issues/359 - `// issue #359 - import { render, screen } from 'mocks/test-utils' - import userEvent from '@testing-library/user-event' - - const testData = { - name: 'John Doe', - email: 'john@doe.com', - password: 'extremeSecret', - } - - const selectors = { - username: () => screen.findByRole('textbox', { name: /username/i }), - email: () => screen.findByRole('textbox', { name: /e-mail/i }), - password: () => screen.findByLabelText(/password/i), - } - - test('this is a valid case', async () => { - render() - userEvent.type(await selectors.username(), testData.name) - userEvent.type(await selectors.email(), testData.email) - userEvent.type(await selectors.password(), testData.password) - // ... - }) - `, - - // edge case for coverage - // valid async query usage without any function defined - // so there is no innermost function scope found - `const element = await findByRole('button')`, - ], - - invalid: [ - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( - (query) => - ({ - code: `// async queries without await operator or then method are not valid - import { render } from '@testing-library/react' - - test("An example test", async () => { - doSomething() - const foo = ${query}('foo') - }); - `, - errors: [{ messageId: 'awaitAsyncQuery', line: 6, column: 21 }], - } as const) - ), - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( - (query) => - ({ - code: `// async screen queries without await operator or then method are not valid - import { render } from '@testing-library/react' - - test("An example test", async () => { - screen.${query}('foo') - }); - `, - errors: [ - { - messageId: 'awaitAsyncQuery', - line: 5, - column: 16, - data: { name: query }, - }, - ], - } as const) - ), - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( - (query) => - ({ - code: ` - import { render } from '@testing-library/react' - - test("An example test", async () => { - doSomething() - const foo = ${query}('foo') - }); - `, - errors: [ - { - messageId: 'awaitAsyncQuery', - line: 6, - column: 21, - data: { name: query }, - }, - ], - } as const) - ), - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( - (query) => - ({ - code: ` - import { render } from '@testing-library/react' - - test("An example test", async () => { - const foo = ${query}('foo') - expect(foo).toBeInTheDocument() - expect(foo).toHaveAttribute('src', 'bar'); - }); - `, - errors: [ - { - messageId: 'awaitAsyncQuery', - line: 5, - column: 21, - data: { name: query }, - }, - ], - } as const) - ), - - // unresolved async queries are not valid (aggressive reporting) - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( - (query) => - ({ - code: ` - import { render } from "another-library" - - test('An example test', async () => { - const example = ${query}("my example") - }) - `, - errors: [{ messageId: 'awaitAsyncQuery', line: 5, column: 27 }], - } as const) - ), - - // unhandled promise from async query function wrapper is invalid - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( - (query) => - ({ - code: ` - function queryWrapper() { - doSomethingElse(); - - return screen.${query}('foo') - } - - test("An invalid example test", () => { - const element = queryWrapper() - }) - - test("An invalid example test", async () => { - const element = await queryWrapper() - }) - `, - errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], - } as const) - ), - // unhandled promise from async query arrow function wrapper is invalid - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( - (query) => - ({ - code: ` - const queryWrapper = () => { - doSomethingElse(); - - return ${query}('foo') - } - - test("An invalid example test", () => { - const element = queryWrapper() - }) - - test("An invalid example test", async () => { - const element = await queryWrapper() - }) - `, - errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], - } as const) - ), - // unhandled promise implicitly returned from async query arrow function wrapper is invalid - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( - (query) => - ({ - code: ` - const queryWrapper = () => screen.${query}('foo') - - test("An invalid example test", () => { - const element = queryWrapper() - }) - - test("An invalid example test", async () => { - const element = await queryWrapper() - }) - `, - errors: [{ messageId: 'asyncQueryWrapper', line: 5, column: 27 }], - } as const) - ), - - // unhandled promise from custom query matching custom-queries setting is invalid - { - settings: { - 'testing-library/custom-queries': ['ByIcon', 'getByComplexText'], - }, - code: ` - test('An invalid example test', () => { - const element = findByIcon('search') - }) - `, - errors: [{ messageId: 'awaitAsyncQuery', line: 3, column: 25 }], - }, - - { - code: `// similar to issue #359 but forcing an error in no-awaited wrapper - import { render, screen } from 'mocks/test-utils' - import userEvent from '@testing-library/user-event' - - const testData = { - name: 'John Doe', - email: 'john@doe.com', - password: 'extremeSecret', - } - - const selectors = { - username: () => screen.findByRole('textbox', { name: /username/i }), - email: () => screen.findByRole('textbox', { name: /e-mail/i }), - password: () => screen.findByLabelText(/password/i), - } - - test('this is a valid case', async () => { - render() - userEvent.type(selectors.username(), testData.name) // <-- unhandled here - userEvent.type(await selectors.email(), testData.email) - userEvent.type(await selectors.password(), testData.password) - // ... - }) - `, - errors: [{ messageId: 'asyncQueryWrapper', line: 19, column: 34 }], - }, - ], -}); diff --git a/tests/lib/rules/await-async-utils.test.ts b/tests/lib/rules/await-async-utils.test.ts index 9c595926..cbf2cdc0 100644 --- a/tests/lib/rules/await-async-utils.test.ts +++ b/tests/lib/rules/await-async-utils.test.ts @@ -4,53 +4,97 @@ import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + ruleTester.run(RULE_NAME, rule, { - valid: [ - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + valid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util directly waited with await operator is valid', async () => { doSomethingElse(); await ${asyncUtil}(() => getByLabelText('email')); }); `, - })), - - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util promise saved in var and waited with await operator is valid', async () => { doSomethingElse(); const aPromise = ${asyncUtil}(() => getByLabelText('email')); await aPromise; }); `, - })), - - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util not called is valid', () => { + expect(${asyncUtil}).toBeDefined(); + }); + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util directly chained with then is valid', () => { doSomethingElse(); ${asyncUtil}(() => getByLabelText('email')).then(() => { console.log('done') }); }); `, - })), - - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util expect chained with .resolves is valid', () => { + doSomethingElse(); + expect(${asyncUtil}(() => getByLabelText('email'))).resolves.toBe("foo"); + }); + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util expect chained with .toResolve is valid', () => { + doSomethingElse(); + expect(${asyncUtil}(() => getByLabelText('email'))).toResolve(); + }); + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util expect chained with jasmine async matchers are valid', () => { + doSomethingElse(); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeRejected(); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeRejectedWith("bar"); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeRejectedWithError("bar"); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeResolved(); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeResolvedTo("bar"); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBePending(); + }); + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util promise saved in var and chained with then is valid', () => { doSomethingElse(); const aPromise = ${asyncUtil}(() => getByLabelText('email')); aPromise.then(() => { console.log('done') }); }); `, - })), - - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util directly returned in arrow function is valid', async () => { const makeCustomWait = () => ${asyncUtil}(() => @@ -58,11 +102,10 @@ ruleTester.run(RULE_NAME, rule, { ); }); `, - })), - - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util explicitly returned in arrow function is valid', async () => { const makeCustomWait = () => { return ${asyncUtil}(() => @@ -71,11 +114,10 @@ ruleTester.run(RULE_NAME, rule, { }; }); `, - })), - - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util returned in regular function is valid', async () => { function makeCustomWait() { return ${asyncUtil}(() => @@ -84,30 +126,29 @@ ruleTester.run(RULE_NAME, rule, { } }); `, - })), - - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util promise saved in var and returned in function is valid', async () => { const makeCustomWait = () => { const aPromise = ${asyncUtil}(() => document.querySelector('div.getOuttaHere') ); - + doSomethingElse(); - + return aPromise; }; }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import { ${asyncUtil} } from 'some-other-library'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { ${asyncUtil} } from 'some-other-library'; // rather than ${testingFramework} test( 'aggressive reporting disabled - util "${asyncUtil}" which is not related to testing library is valid', async () => { @@ -115,13 +156,13 @@ ruleTester.run(RULE_NAME, rule, { ${asyncUtil}(); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import * as asyncUtils from 'some-other-library'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import * as asyncUtils from 'some-other-library'; // rather than ${testingFramework} test( 'aggressive reporting disabled - util "asyncUtils.${asyncUtil}" which is not related to testing library is valid', async () => { @@ -129,10 +170,10 @@ ruleTester.run(RULE_NAME, rule, { asyncUtils.${asyncUtil}(); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util used in with Promise.all() is valid', async () => { await Promise.all([ ${asyncUtil}(callback1), @@ -140,10 +181,10 @@ ruleTester.run(RULE_NAME, rule, { ]); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util used in with Promise.all() with an await is valid', async () => { await Promise.all([ await ${asyncUtil}(callback1), @@ -151,10 +192,10 @@ ruleTester.run(RULE_NAME, rule, { ]); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util used in with Promise.all() with ".then" is valid', async () => { Promise.all([ ${asyncUtil}(callback1), @@ -162,10 +203,10 @@ ruleTester.run(RULE_NAME, rule, { ]).then(() => console.log('foo')); }); `, - })), - { - code: ` - import { waitFor, waitForElementToBeRemoved } from '@testing-library/dom'; + })), + { + code: ` + import { waitFor, waitForElementToBeRemoved } from '${testingFramework}'; test('combining different async methods with Promise.all does not throw an error', async () => { await Promise.all([ waitFor(() => getByLabelText('email')), @@ -173,20 +214,20 @@ ruleTester.run(RULE_NAME, rule, { ]) }); `, - }, - { - code: ` - import { waitForElementToBeRemoved } from '@testing-library/dom'; + }, + { + code: ` + import { waitForElementToBeRemoved } from '${testingFramework}'; test('waitForElementToBeRemoved receiving element rather than callback is valid', async () => { doSomethingElse(); const emailInput = getByLabelText('email'); await waitForElementToBeRemoved(emailInput); }); `, - }, - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + }, + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util used in Promise.allSettled + await expression is valid', async () => { await Promise.allSettled([ ${asyncUtil}(callback1), @@ -194,10 +235,10 @@ ruleTester.run(RULE_NAME, rule, { ]); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util used in Promise.allSettled + then method is valid', async () => { Promise.allSettled([ ${asyncUtil}(callback1), @@ -205,11 +246,11 @@ ruleTester.run(RULE_NAME, rule, { ]).then(() => {}) }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; - + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; + function waitForSomethingAsync() { return ${asyncUtil}(() => somethingAsync()) } @@ -218,143 +259,269 @@ ruleTester.run(RULE_NAME, rule, { await waitForSomethingAsync() }); `, - })), - { - code: ` + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; + + function waitForSomethingAsync() { + return ${asyncUtil}(() => somethingAsync()) + } + + test('handled promise in variable declaration from function wrapping ${asyncUtil} util is valid', async () => { + const result = await waitForSomethingAsync() + expect(result).toBe('foo') + }); + `, + })), + { + code: ` test('using unrelated promises with Promise.all is valid', async () => { Promise.all([ - waitForNotRelatedToTestingLibrary(), + waitForNotRelatedToTestingLibrary(), // completely unrelated to ${testingFramework} promise1, await foo().then(() => baz()) ]) }) `, - }, - - // edge case for coverage - // valid async query usage without any function defined - // so there is no innermost function scope found - ` - import { waitFor } from '@testing-library/dom'; - test('edge case for no innermost function scope', () => { - const foo = waitFor - }) - `, - ], - invalid: [ - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + }, + { + // edge case for coverage + // valid async query usage without any function defined + // so there is no innermost function scope found + code: ` + import { waitFor } from '${testingFramework}'; + test('edge case for no innermost function scope', () => { + const foo = waitFor + }) + `, + }, + { + // edge case for coverage: CallExpressions without deepest identifiers + code: ` + import { waitFor } from '${testingFramework}'; + test('coverage test for CallExpressions without identifiers', () => { + const asyncUtil = waitFor + + // These CallExpressions have no deepest identifier: + const funcs = [() => console.log('test')] + const obj = { [Symbol.iterator]: () => 'symbol' } + + funcs[0]() + obj[Symbol.iterator]() + (function() { return 'iife' })() + }); + `, + }, + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('destructuring an async function wrapper & handling it later is valid', () => { + const { user, waitForAsyncUtil } = setup(); + await waitForAsyncUtil(); + + const myAlias = waitForAsyncUtil; + const myOtherAlias = myAlias; + await myAlias(); + await myOtherAlias(); + + const { ...clone } = setup(); + await clone.waitForAsyncUtil(); + + const { waitForAsyncUtil: myDestructuredAlias } = setup(); + await myDestructuredAlias(); + + const { user, ...rest } = setup(); + await rest.waitForAsyncUtil(); + + await setup().waitForAsyncUtil(); + }); + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import React from 'react'; + import { render, act } from '${testingFramework}'; + + const doWithAct = async (timeout) => { + await act(async () => await ${asyncUtil}(screen.getByTestId('my-test'))); + }; + + describe('Component', () => { + const mock = jest.fn(); + + it('test', async () => { + let Component = () => { + mock(1); + return
; + }; + render(); + + await doWithAct(500); + + const myNumberTestVar = 1; + const myBooleanTestVar = false; + const myArrayTestVar = [1, 2]; + const myStringTestVar = 'hello world'; + const myObjectTestVar = { hello: 'world' }; + + expect(mock).toHaveBeenCalledWith(myNumberTestVar); + }); + }); + `, + })), + ]), + invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util not waited is invalid', () => { doSomethingElse(); ${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [ - { - line: 5, - column: 11, - messageId: 'awaitAsyncUtil', - data: { name: asyncUtil }, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + errors: [ + { + line: 5, + column: 11, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util not waited is invalid', () => { doSomethingElse(); const el = ${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [ - { - line: 5, - column: 22, - messageId: 'awaitAsyncUtil', - data: { name: asyncUtil }, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import * as asyncUtil from '@testing-library/dom'; + errors: [ + { + line: 5, + column: 22, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import * as asyncUtil from '${testingFramework}'; test('asyncUtil.${asyncUtil} util not handled is invalid', () => { doSomethingElse(); asyncUtil.${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [ - { - line: 5, - column: 21, - messageId: 'awaitAsyncUtil', - data: { name: asyncUtil }, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + errors: [ + { + line: 5, + column: 21, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('${asyncUtil} util promise saved not handled is invalid', () => { doSomethingElse(); const aPromise = ${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [ - { - line: 5, - column: 28, - messageId: 'awaitAsyncUtil', - data: { name: asyncUtil }, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + errors: [ + { + line: 5, + column: 28, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('several ${asyncUtil} utils not handled are invalid', () => { const aPromise = ${asyncUtil}(() => getByLabelText('username')); doSomethingElse(aPromise); ${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [ - { - line: 4, - column: 28, - messageId: 'awaitAsyncUtil', - data: { name: asyncUtil }, - }, - { - line: 6, - column: 11, - messageId: 'awaitAsyncUtil', - data: { name: asyncUtil }, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import { ${asyncUtil}, render } from '@testing-library/dom'; - + errors: [ + { + line: 4, + column: 28, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + { + line: 6, + column: 11, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('unhandled expression that evaluates to promise is invalid', () => { + const aPromise = ${asyncUtil}(() => getByLabelText('username')); + doSomethingElse(aPromise); + ${asyncUtil}(() => getByLabelText('email')); + }); + `, + errors: [ + { + line: 4, + column: 28, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + { + line: 6, + column: 11, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil}, render } from '${testingFramework}'; + function waitForSomethingAsync() { return ${asyncUtil}(() => somethingAsync()) } @@ -364,21 +531,47 @@ ruleTester.run(RULE_NAME, rule, { waitForSomethingAsync() }); `, - errors: [ - { - messageId: 'asyncUtilWrapper', - line: 10, - column: 11, - data: { name: 'waitForSomethingAsync' }, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import { ${asyncUtil} } from 'some-other-library'; + errors: [ + { + messageId: 'asyncUtilWrapper', + line: 10, + column: 11, + data: { name: 'waitForSomethingAsync' }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil}, render } from '${testingFramework}'; + + function waitForSomethingAsync() { + return ${asyncUtil}(() => somethingAsync()) + } + + test('unhandled promise in variable declaration from function wrapping ${asyncUtil} util is invalid', async () => { + render() + const result = waitForSomethingAsync() + expect(result).toBe('foo') + }); + `, + errors: [ + { + messageId: 'asyncUtilWrapper', + line: 10, + column: 24, + data: { name: 'waitForSomethingAsync' }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil} } from 'some-other-library'; // rather than ${testingFramework} test( 'aggressive reporting - util "${asyncUtil}" which is not related to testing library is invalid', async () => { @@ -386,22 +579,22 @@ ruleTester.run(RULE_NAME, rule, { ${asyncUtil}(); }); `, - errors: [ - { - line: 7, - column: 11, - messageId: 'awaitAsyncUtil', - data: { name: asyncUtil }, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import { ${asyncUtil}, render } from '@testing-library/dom'; - + errors: [ + { + line: 7, + column: 11, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil}, render } from '${testingFramework}'; + function waitForSomethingAsync() { return ${asyncUtil}(() => somethingAsync()) } @@ -411,21 +604,22 @@ ruleTester.run(RULE_NAME, rule, { const el = waitForSomethingAsync() }); `, - errors: [ - { - messageId: 'asyncUtilWrapper', - line: 10, - column: 22, - data: { name: 'waitForSomethingAsync' }, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import * as asyncUtils from 'some-other-library'; + errors: [ + { + messageId: 'asyncUtilWrapper', + line: 10, + column: 22, + data: { name: 'waitForSomethingAsync' }, + }, + ], + }) as const + ), + + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import * as asyncUtils from 'some-other-library'; // rather than ${testingFramework} test( 'aggressive reporting - util "asyncUtils.${asyncUtil}" which is not related to testing library is invalid', async () => { @@ -433,15 +627,195 @@ ruleTester.run(RULE_NAME, rule, { asyncUtils.${asyncUtil}(); }); `, - errors: [ - { - line: 7, - column: 22, - messageId: 'awaitAsyncUtil', - data: { name: asyncUtil }, - }, - ], - } as const) - ), - ], + errors: [ + { + line: 7, + column: 22, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + const { user, waitForAsyncUtil } = setup(); + waitForAsyncUtil(); + }); + `, + errors: [ + { + line: 15, + column: 11, + messageId: 'asyncUtilWrapper', + data: { name: 'waitForAsyncUtil' }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + const { user, waitForAsyncUtil } = setup(); + const myAlias = waitForAsyncUtil; + myAlias(); + }); + `, + errors: [ + { + line: 16, + column: 11, + messageId: 'asyncUtilWrapper', + data: { name: 'myAlias' }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + const { ...clone } = setup(); + clone.waitForAsyncUtil(); + }); + `, + errors: [ + { + line: 15, + column: 17, + messageId: 'asyncUtilWrapper', + data: { name: 'waitForAsyncUtil' }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + const { waitForAsyncUtil: myAlias } = setup(); + myAlias(); + }); + `, + errors: [ + { + line: 15, + column: 11, + messageId: 'asyncUtilWrapper', + data: { name: 'myAlias' }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + setup().waitForAsyncUtil(); + }); + `, + errors: [ + { + line: 14, + column: 19, + messageId: 'asyncUtilWrapper', + data: { name: 'waitForAsyncUtil' }, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + const myAlias = setup().waitForAsyncUtil; + myAlias(); + }); + `, + errors: [ + { + line: 15, + column: 11, + messageId: 'asyncUtilWrapper', + data: { name: 'myAlias' }, + }, + ], + }) as const + ), + ]), }); diff --git a/tests/lib/rules/await-fire-event.test.ts b/tests/lib/rules/await-fire-event.test.ts deleted file mode 100644 index 2369539f..00000000 --- a/tests/lib/rules/await-fire-event.test.ts +++ /dev/null @@ -1,345 +0,0 @@ -import rule, { RULE_NAME } from '../../../lib/rules/await-fire-event'; -import { createRuleTester } from '../test-utils'; - -const ruleTester = createRuleTester(); - -const COMMON_FIRE_EVENT_METHODS: string[] = [ - 'click', - 'change', - 'focus', - 'blur', - 'keyDown', -]; - -ruleTester.run(RULE_NAME, rule, { - valid: [ - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test('fire event method not called is valid', () => { - fireEvent.${fireEventMethod} - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test('await promise from fire event method is valid', async () => { - await fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test('await several promises from fire event methods is valid', async () => { - await fireEvent.${fireEventMethod}(getByLabelText('username')) - await fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test('await promise kept in a var from fire event method is valid', async () => { - const promise = fireEvent.${fireEventMethod}(getByLabelText('username')) - await promise - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test('chain then method to promise from fire event method is valid', async (done) => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - .then(() => { done() }) - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test('chain then method to several promises from fire event methods is valid', async (done) => { - fireEvent.${fireEventMethod}(getByLabelText('username')).then(() => { - fireEvent.${fireEventMethod}(getByLabelText('username')).then(() => { done() }) - }) - }) - `, - })), - `import { fireEvent } from '@testing-library/vue' - - test('fireEvent methods wrapped with Promise.all are valid', async () => { - await Promise.all([ - fireEvent.blur(getByText('Click me')), - fireEvent.click(getByText('Click me')), - ]) - }) - `, - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test('return promise from fire event methods is valid', () => { - function triggerEvent() { - doSomething() - return fireEvent.${fireEventMethod}(getByLabelText('username')) - } - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test('await promise returned from function wrapping fire event method is valid', () => { - function triggerEvent() { - doSomething() - return fireEvent.${fireEventMethod}(getByLabelText('username')) - } - - await triggerEvent() - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import { fireEvent } from 'somewhere-else' - test('unhandled promise from fire event not related to TL is valid', async () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import { fireEvent } from 'test-utils' - test('await promise from fire event method imported from custom module is valid', async () => { - await fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - })), - - // edge case for coverage: - // valid use case without call expression - // so there is no innermost function scope found - ` - import { fireEvent } from 'test-utils' - test('edge case for innermost function without call expression', async () => { - function triggerEvent() { - doSomething() - return fireEvent.focus(getByLabelText('username')) - } - - const reassignedFunction = triggerEvent - }) - `, - ], - - invalid: [ - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test('unhandled promise from fire event method is invalid', async () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 4, - column: 9, - endColumn: 19 + fireEventMethod.length, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import { fireEvent as testingLibraryFireEvent } from '@testing-library/vue' - test('unhandled promise from aliased fire event method is invalid', async () => { - testingLibraryFireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 4, - column: 9, - endColumn: 33 + fireEventMethod.length, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import * as testingLibrary from '@testing-library/vue' - test('unhandled promise from wildcard imported fire event method is invalid', async () => { - testingLibrary.fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 4, - column: 9, - endColumn: 34 + fireEventMethod.length, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test('several unhandled promises from fire event methods is invalid', async () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 4, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - { - line: 5, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import { fireEvent } from '@testing-library/vue' - test('unhandled promise from fire event method with aggressive reporting opted-out is invalid', async () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 4, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import { fireEvent } from 'test-utils' - test( - 'unhandled promise from fire event method imported from custom module with aggressive reporting opted-out is invalid', - () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 6, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import { fireEvent } from '@testing-library/vue' - test( - 'unhandled promise from fire event method imported from default module with aggressive reporting opted-out is invalid', - () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 6, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test( - 'unhandled promise from fire event method kept in a var is invalid', - () => { - const promise = fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 6, - column: 25, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import { fireEvent } from '@testing-library/vue' - test('unhandled promise returned from function wrapping fire event method is invalid', () => { - function triggerEvent() { - doSomething() - return fireEvent.${fireEventMethod}(getByLabelText('username')) - } - - triggerEvent() - }) - `, - errors: [ - { - line: 9, - column: 9, - messageId: 'fireEventWrapper', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ], -}); diff --git a/tests/lib/rules/consistent-data-testid.test.ts b/tests/lib/rules/consistent-data-testid.test.ts index 331b5709..a2cdc46c 100644 --- a/tests/lib/rules/consistent-data-testid.test.ts +++ b/tests/lib/rules/consistent-data-testid.test.ts @@ -1,32 +1,35 @@ -import type { TSESLint } from '@typescript-eslint/experimental-utils'; +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; import rule, { - MessageIds, - Options, - RULE_NAME, + MessageIds, + Options, + RULE_NAME, } from '../../../lib/rules/consistent-data-testid'; import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); -type ValidTestCase = TSESLint.ValidTestCase; -type InvalidTestCase = TSESLint.InvalidTestCase; -type TestCase = InvalidTestCase | ValidTestCase; +type RuleValidTestCase = ValidTestCase; +type RuleInvalidTestCase = InvalidTestCase; +type TestCase = RuleValidTestCase | RuleInvalidTestCase; const disableAggressiveReporting = (array: T[]): T[] => - array.map((testCase) => ({ - ...testCase, - settings: { - 'testing-library/utils-module': 'off', - 'testing-library/custom-renders': 'off', - 'testing-library/custom-queries': 'off', - }, - })); + array.map((testCase) => ({ + ...testCase, + settings: { + 'testing-library/utils-module': 'off', + 'testing-library/custom-renders': 'off', + 'testing-library/custom-queries': 'off', + }, + })); -const validTestCases: ValidTestCase[] = [ - { - code: ` +const validTestCases: RuleValidTestCase[] = [ + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -35,12 +38,12 @@ const validTestCases: ValidTestCase[] = [ ) }; `, - options: [{ testIdPattern: 'cool' }], - }, - { - code: ` + options: [{ testIdPattern: 'cool' }], + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -49,12 +52,12 @@ const validTestCases: ValidTestCase[] = [ ) }; `, - options: [{ testIdPattern: 'cool' }], - }, - { - code: ` + options: [{ testIdPattern: 'cool' }], + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -63,17 +66,17 @@ const validTestCases: ValidTestCase[] = [ ) }; `, - options: [ - { - testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', - }, - ], - filename: '/my/cool/file/path/Awesome.js', - }, - { - code: ` + options: [ + { + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + }, + ], + filename: '/my/cool/file/path/Awesome.js', + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -82,17 +85,17 @@ const validTestCases: ValidTestCase[] = [ ) }; `, - options: [ - { - testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', - }, - ], - filename: '/my/cool/file/path/Awesome.js', - }, - { - code: ` + options: [ + { + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + }, + ], + filename: '/my/cool/file/path/Awesome.js', + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -101,17 +104,17 @@ const validTestCases: ValidTestCase[] = [ ) }; `, - options: [ - { - testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', - }, - ], - filename: '/my/cool/file/Parent/index.js', - }, - { - code: ` + options: [ + { + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + }, + ], + filename: '/my/cool/file/Parent/index.js', + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -120,17 +123,17 @@ const validTestCases: ValidTestCase[] = [ ) }; `, - options: [ - { - testIdPattern: '{fileName}', - }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - }, - { - code: ` + options: [ + { + testIdPattern: '{fileName}', + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -139,17 +142,17 @@ const validTestCases: ValidTestCase[] = [ ) }; `, - options: [ - { - testIdPattern: '^right(.*)$', - testIdAttribute: 'custom-attr', - }, - ], - }, - { - code: ` + options: [ + { + testIdPattern: '^right(.*)$', + testIdAttribute: 'custom-attr', + }, + ], + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -158,17 +161,17 @@ const validTestCases: ValidTestCase[] = [ ) }; `, - options: [ - { - testIdPattern: '^right(.*)$', - testIdAttribute: ['custom-attr', 'another-custom-attr'], - }, - ], - }, - { - code: ` + options: [ + { + testIdPattern: '^right(.*)$', + testIdAttribute: ['custom-attr', 'another-custom-attr'], + }, + ], + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -177,18 +180,18 @@ const validTestCases: ValidTestCase[] = [ ) }; `, - options: [ - { - testIdPattern: '{fileName}', - testIdAttribute: 'data-test-id', - }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - }, - { - code: ` + options: [ + { + testIdPattern: '{fileName}', + testIdAttribute: 'data-test-id', + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { const dynamicTestId = 'somethingDynamic'; return ( @@ -198,14 +201,75 @@ const validTestCases: ValidTestCase[] = [ ) }; `, - options: [{ testIdPattern: 'somethingElse' }], - }, + options: [{ testIdPattern: 'somethingElse' }], + }, + // To fix issue 509, https://github.com/testing-library/eslint-plugin-testing-library/issues/509 + // Gatsby.js ja Next.js use square brackets in filenames to create dynamic routes + { + code: ` + import React from 'react'; + + const TestComponent = props => { + return ( +
+ Hello +
+ ) + }; + `, + options: [ + { + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + }, + ], + filename: '/my/cool/file/path/[client-only].js', + }, + { + code: ` + import React from 'react'; + + const TestComponent = props => { + return ( +
+ Hello +
+ ) + }; + `, + options: [ + { + // should work if given the {fileName} placeholder + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + }, + ], + filename: '/my/cool/file/path/[...wildcard].js', + }, + { + code: ` + import React from 'react'; + + const TestComponent = props => { + return ( +
+ Hello +
+ ) + }; + `, + options: [ + { + // should work also if not given the {fileName} placeholder + testIdPattern: '^(__([A-Z]+[a-z]*?)+)*$', + }, + ], + filename: '/my/cool/file/path/[...wildcard].js', + }, ]; -const invalidTestCases: InvalidTestCase[] = [ - { - code: ` +const invalidTestCases: RuleInvalidTestCase[] = [ + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -214,22 +278,23 @@ const invalidTestCases: InvalidTestCase[] = [ ) }; `, - options: [{ testIdPattern: 'error' }], - errors: [ - { - messageId: 'consistentDataTestId', - data: { - attr: 'data-testid', - value: 'Awesome__CoolStuff', - regex: '/error/', - }, - }, - ], - }, - { - code: ` + options: [{ testIdPattern: 'error' }], + errors: [ + { + messageId: 'consistentDataTestId', + data: { + attr: 'data-testid', + value: 'Awesome__CoolStuff', + regex: '/error/', + message: '', + }, + }, + ], + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -238,27 +303,28 @@ const invalidTestCases: InvalidTestCase[] = [ ) }; `, - options: [ - { - testIdPattern: 'matchMe', - }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - errors: [ - { - messageId: 'consistentDataTestId', - data: { - attr: 'data-testid', - value: 'Nope', - regex: '/matchMe/', - }, - }, - ], - }, - { - code: ` + options: [ + { + testIdPattern: 'matchMe', + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + errors: [ + { + messageId: 'consistentDataTestId', + data: { + attr: 'data-testid', + value: 'Nope', + regex: '/matchMe/', + message: '', + }, + }, + ], + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -267,28 +333,29 @@ const invalidTestCases: InvalidTestCase[] = [ ) }; `, - options: [ - { - testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', - testIdAttribute: 'my-custom-attr', - }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - errors: [ - { - messageId: 'consistentDataTestId', - data: { - attr: 'my-custom-attr', - value: 'WrongComponent__cool', - regex: '/^Parent(__([A-Z]+[a-z]*?)+)*$/', - }, - }, - ], - }, - { - code: ` + options: [ + { + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + testIdAttribute: 'my-custom-attr', + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + errors: [ + { + messageId: 'consistentDataTestId', + data: { + attr: 'my-custom-attr', + value: 'WrongComponent__cool', + regex: '/^Parent(__([A-Z]+[a-z]*?)+)*$/', + message: '', + }, + }, + ], + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -297,36 +364,37 @@ const invalidTestCases: InvalidTestCase[] = [ ) }; `, - options: [ - { - testIdPattern: '^right$', - testIdAttribute: ['custom-attr', 'another-custom-attr'], - }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - errors: [ - { - messageId: 'consistentDataTestId', - data: { - attr: 'custom-attr', - value: 'wrong', - regex: '/^right$/', - }, - }, - { - messageId: 'consistentDataTestId', - data: { - attr: 'another-custom-attr', - value: 'wrong', - regex: '/^right$/', - }, - }, - ], - }, - { - code: ` + options: [ + { + testIdPattern: '^right$', + testIdAttribute: ['custom-attr', 'another-custom-attr'], + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + errors: [ + { + messageId: 'consistentDataTestId', + data: { + attr: 'custom-attr', + value: 'wrong', + regex: '/^right$/', + }, + }, + { + messageId: 'consistentDataTestId', + data: { + attr: 'another-custom-attr', + value: 'wrong', + regex: '/^right$/', + message: '', + }, + }, + ], + }, + { + code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -335,29 +403,61 @@ const invalidTestCases: InvalidTestCase[] = [ ) }; `, - options: [ - { - testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', - }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - errors: [ - { - messageId: 'consistentDataTestId', - data: { - attr: 'data-testid', - value: 'WrongComponent__cool', - regex: '/^Parent(__([A-Z]+[a-z]*?)+)*$/', - }, - }, - ], - }, + options: [ + { + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + errors: [ + { + messageId: 'consistentDataTestId', + data: { + attr: 'data-testid', + value: 'WrongComponent__cool', + regex: '/^Parent(__([A-Z]+[a-z]*?)+)*$/', + message: '', + }, + }, + ], + }, + { + code: ` // test for custom message + import React from 'react'; + + const TestComponent = props => { + return ( +
+ Hello +
+ ) + }; + `, + options: [ + { + testIdPattern: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', //kebab-case + customMessage: 'Please use kebab-cased data-testid values.', + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + errors: [ + { + messageId: 'consistentDataTestIdCustomMessage', + data: { + attr: 'data-testid', + value: 'snake_case_value', + regex: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', + message: 'Please use kebab-cased data-testid values.', + }, + }, + ], + }, ]; ruleTester.run(RULE_NAME, rule, { - valid: [...validTestCases, ...disableAggressiveReporting(validTestCases)], - invalid: [ - ...invalidTestCases, - ...disableAggressiveReporting(invalidTestCases), - ], + valid: [...validTestCases, ...disableAggressiveReporting(validTestCases)], + invalid: [ + ...invalidTestCases, + ...disableAggressiveReporting(invalidTestCases), + ], }); diff --git a/tests/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index e63c130b..da871acc 100644 --- a/tests/lib/rules/no-await-sync-events.test.ts +++ b/tests/lib/rules/no-await-sync-events.test.ts @@ -4,157 +4,199 @@ import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); const FIRE_EVENT_FUNCTIONS = [ - 'copy', - 'cut', - 'paste', - 'compositionEnd', - 'compositionStart', - 'compositionUpdate', - 'keyDown', - 'keyPress', - 'keyUp', - 'focus', - 'blur', - 'focusIn', - 'focusOut', - 'change', - 'input', - 'invalid', - 'submit', - 'reset', - 'click', - 'contextMenu', - 'dblClick', - 'drag', - 'dragEnd', - 'dragEnter', - 'dragExit', - 'dragLeave', - 'dragOver', - 'dragStart', - 'drop', - 'mouseDown', - 'mouseEnter', - 'mouseLeave', - 'mouseMove', - 'mouseOut', - 'mouseOver', - 'mouseUp', - 'popState', - 'select', - 'touchCancel', - 'touchEnd', - 'touchMove', - 'touchStart', - 'scroll', - 'wheel', - 'abort', - 'canPlay', - 'canPlayThrough', - 'durationChange', - 'emptied', - 'encrypted', - 'ended', - 'loadedData', - 'loadedMetadata', - 'loadStart', - 'pause', - 'play', - 'playing', - 'progress', - 'rateChange', - 'seeked', - 'seeking', - 'stalled', - 'suspend', - 'timeUpdate', - 'volumeChange', - 'waiting', - 'load', - 'error', - 'animationStart', - 'animationEnd', - 'animationIteration', - 'transitionEnd', - 'doubleClick', - 'pointerOver', - 'pointerEnter', - 'pointerDown', - 'pointerMove', - 'pointerUp', - 'pointerCancel', - 'pointerOut', - 'pointerLeave', - 'gotPointerCapture', - 'lostPointerCapture', + 'copy', + 'cut', + 'paste', + 'compositionEnd', + 'compositionStart', + 'compositionUpdate', + 'keyDown', + 'keyPress', + 'keyUp', + 'focus', + 'blur', + 'focusIn', + 'focusOut', + 'change', + 'input', + 'invalid', + 'submit', + 'reset', + 'click', + 'contextMenu', + 'dblClick', + 'drag', + 'dragEnd', + 'dragEnter', + 'dragExit', + 'dragLeave', + 'dragOver', + 'dragStart', + 'drop', + 'mouseDown', + 'mouseEnter', + 'mouseLeave', + 'mouseMove', + 'mouseOut', + 'mouseOver', + 'mouseUp', + 'popState', + 'select', + 'touchCancel', + 'touchEnd', + 'touchMove', + 'touchStart', + 'scroll', + 'wheel', + 'abort', + 'canPlay', + 'canPlayThrough', + 'durationChange', + 'emptied', + 'encrypted', + 'ended', + 'loadedData', + 'loadedMetadata', + 'loadStart', + 'pause', + 'play', + 'playing', + 'progress', + 'rateChange', + 'seeked', + 'seeking', + 'stalled', + 'suspend', + 'timeUpdate', + 'volumeChange', + 'waiting', + 'load', + 'error', + 'animationStart', + 'animationEnd', + 'animationIteration', + 'transitionEnd', + 'doubleClick', + 'pointerOver', + 'pointerEnter', + 'pointerDown', + 'pointerMove', + 'pointerUp', + 'pointerCancel', + 'pointerOut', + 'pointerLeave', + 'gotPointerCapture', + 'lostPointerCapture', +]; +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', ]; const USER_EVENT_SYNC_FUNCTIONS = [ - 'clear', - 'click', - 'dblClick', - 'selectOptions', - 'deselectOptions', - 'upload', - // 'type', - // 'keyboard', - 'tab', - 'paste', - 'hover', - 'unhover', + 'clear', + 'click', + 'dblClick', + 'selectOptions', + 'deselectOptions', + 'upload', + // 'type', + // 'keyboard', + 'tab', + 'paste', + 'hover', + 'unhover', ]; ruleTester.run(RULE_NAME, rule, { - valid: [ - // sync fireEvents methods without await are valid - ...FIRE_EVENT_FUNCTIONS.map((func) => ({ - code: `() => { + valid: [ + // sync fireEvents methods without await are valid + ...FIRE_EVENT_FUNCTIONS.map((func) => ({ + code: `() => { fireEvent.${func}('foo') } `, - })), - // sync userEvent methods without await are valid - ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ - code: `() => { + })), + // sync userEvent methods without await are valid + ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ + code: `() => { userEvent.${func}('foo') } `, - })), - { - code: `() => { + })), + { + code: `() => { userEvent.type(element, 'foo') } `, - }, - { - code: `() => { + }, + { + code: `() => { userEvent.keyboard('foo') } `, - }, - { - code: `() => { + }, + { + code: `() => { await userEvent.type(element, 'bar', {delay: 1234}) } `, - }, - { - code: `() => { + }, + { + code: `() => { + await userEvent.type(element, 'bar', {delay: null}) + } + `, + }, + { + code: `() => { await userEvent.keyboard('foo', {delay: 1234}) } `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + code: `async() => { + const delay = 10 + await userEvent.keyboard('foo', {delay}) + } + `, + }, + { + code: `async() => { + const delay = null + await userEvent.keyboard('foo', {delay}) + } + `, + }, + { + code: `async() => { + const delay = 10 + await userEvent.type(element, text, {delay}) + } + `, + }, + { + code: `async() => { + let delay = 0 + delay = 10 + await userEvent.type(element, text, {delay}) + } + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { fireEvent } from 'somewhere-else'; test('should not report fireEvent.click() not related to Testing Library', async() => { await fireEvent.click('foo'); }); `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { fireEvent as renamedFireEvent } from 'somewhere-else'; import renamedUserEvent from '@testing-library/user-event'; import { fireEvent, userEvent } from 'somewhere-else' @@ -165,100 +207,139 @@ ruleTester.run(RULE_NAME, rule, { await userEvent.keyboard('foo', { delay: 5 }); }); `, - }, - ], + }, + + // valid tests for fire-event when only user-event set in eventModules + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => + FIRE_EVENT_FUNCTIONS.map((func) => ({ + code: ` + import { fireEvent } from '${testingFramework}'; + test('should not report fireEvent.${func} sync event awaited', async() => { + await fireEvent.${func}('foo'); + }); + `, + options: [{ eventModules: ['user-event'] }], + })) + ), - invalid: [ - // sync fireEvent methods with await operator are not valid - ...FIRE_EVENT_FUNCTIONS.map( - (func) => - ({ - code: ` - import { fireEvent } from '@testing-library/framework'; + // valid tests for user-event when only fire-event set in eventModules + ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ + code: ` + import userEvent from '@testing-library/user-event'; + test('should not report userEvent.${func} sync event awaited', async() => { + await userEvent.${func}('foo'); + }); + `, + options: [{ eventModules: ['fire-event'] }], + })), + + // valid tests for user-event with default options (user-event disabled) + ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ + code: ` + import userEvent from '@testing-library/user-event'; + test('should not report userEvent.${func} by default', async() => { + await userEvent.${func}('foo'); + }); + `, + })), + ], + + invalid: [ + // sync fireEvent methods with await operator are not valid + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => + FIRE_EVENT_FUNCTIONS.map( + (func) => + ({ + code: ` + import { fireEvent } from '${testingFramework}'; test('should report fireEvent.${func} sync event awaited', async() => { await fireEvent.${func}('foo'); }); `, - errors: [ - { - line: 4, - column: 17, - messageId: 'noAwaitSyncEvents', - data: { name: `fireEvent.${func}` }, - }, - ], - } as const) - ), - // sync userEvent sync methods with await operator are not valid - ...USER_EVENT_SYNC_FUNCTIONS.map( - (func) => - ({ - code: ` + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: `fireEvent.${func}` }, + }, + ], + }) as const + ) + ), + // sync userEvent sync methods with await operator are not valid + ...USER_EVENT_SYNC_FUNCTIONS.map( + (func) => + ({ + code: ` import userEvent from '@testing-library/user-event'; test('should report userEvent.${func} sync event awaited', async() => { await userEvent.${func}('foo'); }); `, - errors: [ - { - line: 4, - column: 17, - messageId: 'noAwaitSyncEvents', - data: { name: `userEvent.${func}` }, - }, - ], - } as const) - ), + options: [{ eventModules: ['user-event'] }], + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: `userEvent.${func}` }, + }, + ], + }) as const + ), - { - code: ` + { + code: ` import userEvent from '@testing-library/user-event'; test('should report async events without delay awaited', async() => { await userEvent.type('foo', 'bar'); await userEvent.keyboard('foo'); }); `, - errors: [ - { - line: 4, - column: 17, - messageId: 'noAwaitSyncEvents', - data: { name: 'userEvent.type' }, - }, - { - line: 5, - column: 17, - messageId: 'noAwaitSyncEvents', - data: { name: 'userEvent.keyboard' }, - }, - ], - }, - { - code: ` + options: [{ eventModules: ['user-event'] }], + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.keyboard' }, + }, + ], + }, + { + code: ` import userEvent from '@testing-library/user-event'; test('should report async events with 0 delay awaited', async() => { await userEvent.type('foo', 'bar', { delay: 0 }); await userEvent.keyboard('foo', { delay: 0 }); }); `, - errors: [ - { - line: 4, - column: 17, - messageId: 'noAwaitSyncEvents', - data: { name: 'userEvent.type' }, - }, - { - line: 5, - column: 17, - messageId: 'noAwaitSyncEvents', - data: { name: 'userEvent.keyboard' }, - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + options: [{ eventModules: ['user-event'] }], + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.keyboard' }, + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { fireEvent as renamedFireEvent } from 'test-utils'; import renamedUserEvent from '@testing-library/user-event'; @@ -268,26 +349,81 @@ ruleTester.run(RULE_NAME, rule, { await renamedUserEvent.keyboard('foo', { delay: 0 }); }); `, - errors: [ - { - line: 6, - column: 17, - messageId: 'noAwaitSyncEvents', - data: { name: 'renamedFireEvent.click' }, - }, - { - line: 7, - column: 17, - messageId: 'noAwaitSyncEvents', - data: { name: 'renamedUserEvent.type' }, - }, - { - line: 8, - column: 17, - messageId: 'noAwaitSyncEvents', - data: { name: 'renamedUserEvent.keyboard' }, - }, - ], - }, - ], + options: [{ eventModules: ['user-event', 'fire-event'] }], + errors: [ + { + line: 6, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'renamedFireEvent.click' }, + }, + { + line: 7, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'renamedUserEvent.type' }, + }, + { + line: 8, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'renamedUserEvent.keyboard' }, + }, + ], + }, + { + code: `async() => { + const delay = 0 + await userEvent.type('foo', { delay }); + } + `, + options: [{ eventModules: ['user-event'] }], + errors: [ + { + line: 3, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + ], + }, + { + code: `async() => { + const delay = 0 + const somethingElse = true + const skipHover = true + await userEvent.type('foo', { delay, skipHover }); + } + `, + options: [{ eventModules: ['user-event'] }], + errors: [ + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + ], + }, + { + code: `async() => { + let delay = 0 + const somethingElse = true + const skipHover = true + delay = 15 + delay = 0 + await userEvent.type('foo', { delay, skipHover }); + } + `, + options: [{ eventModules: ['user-event'] }], + errors: [ + { + line: 7, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + ], + }, + ], }); diff --git a/tests/lib/rules/no-await-sync-queries.test.ts b/tests/lib/rules/no-await-sync-queries.test.ts new file mode 100644 index 00000000..0896f817 --- /dev/null +++ b/tests/lib/rules/no-await-sync-queries.test.ts @@ -0,0 +1,274 @@ +import rule, { RULE_NAME } from '../../../lib/rules/no-await-sync-queries'; +import { + SYNC_QUERIES_COMBINATIONS, + ASYNC_QUERIES_COMBINATIONS, +} from '../../../lib/utils'; +import { createRuleTester } from '../test-utils'; + +const ruleTester = createRuleTester(); + +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + +ruleTester.run(RULE_NAME, rule, { + valid: [ + // sync queries without await are valid + ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ + code: `() => { + const element = ${query}('foo') + } + `, + })), + // custom sync queries without await are valid + `() => { + const element = getByIcon('search') + } + `, + `() => { + const element = queryByIcon('search') + } + `, + `() => { + const element = getAllByIcon('search') + } + `, + `() => { + const element = queryAllByIcon('search') + } + `, + `async () => { + await waitFor(() => { + getByText('search'); + }); + } + `, + + // awaited custom sync query not matching custom-queries setting is valid + { + settings: { + 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'], + }, + code: ` + test('A valid example test', async () => { + const element = await getByIcon('search') + }) + `, + }, + + // sync queries without await inside assert are valid + ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ + code: `() => { + expect(${query}('foo')).toBeEnabled() + } + `, + })), + + // async queries with await operator are valid + ...ASYNC_QUERIES_COMBINATIONS.map((query) => ({ + code: `async () => { + const element = await ${query}('foo') + } + `, + })), + + // async queries with then method are valid + ...ASYNC_QUERIES_COMBINATIONS.map((query) => ({ + code: `() => { + ${query}('foo').then(() => {}); + } + `, + })), + + // sync query awaited but not related to custom module is invalid but not reported + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { screen } from 'somewhere-else' + () => { + const element = await screen.getByRole('button') + } + `, + }, + + // https://github.com/testing-library/eslint-plugin-testing-library/issues/276 + ` + // sync query within call expression but not part of the callee + const chooseElementFromSomewhere = async (text, getAllByLabelText) => { + const someElement = getAllByLabelText(text)[0].parentElement; + // ... + await someOtherAsyncFunction(); + }; + + await chooseElementFromSomewhere('someTextToUseInAQuery', getAllByLabelText); + `, + + `// edge case for coverage: + // valid use case without call expression + // so there is no innermost function scope found + await test('edge case for no innermost function scope', () => { + const foo = getAllByLabelText + }) + `, + + `// edge case for coverage: CallExpression without deepest Identifier + await someList[0](); + `, + + `// element is removed + test('movie title no longer present in DOM', async () => { + await waitForElementToBeRemoved(() => queryByText('the mummy')) + }) + `, + ], + + invalid: [ + // sync queries with await operator are not valid + ...SYNC_QUERIES_COMBINATIONS.map( + (query) => + ({ + code: `async () => { + const element = await ${query}('foo') + } + `, + errors: [ + { + messageId: 'noAwaitSyncQuery', + line: 2, + column: 31, + }, + ], + }) as const + ), + // custom sync queries with await operator are not valid + { + code: ` + async () => { + const element = await getByIcon('search') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 31 }], + }, + { + code: ` + async () => { + const element = await queryByIcon('search') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 31 }], + }, + { + code: ` + async () => { + const element = await screen.getAllByIcon('search') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 38 }], + }, + { + code: ` + async () => { + const element = await screen.queryAllByIcon('search') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 38 }], + }, + // sync queries with await operator inside assert are not valid + ...SYNC_QUERIES_COMBINATIONS.map( + (query) => + ({ + code: `async () => { + expect(await ${query}('foo')).toBeEnabled() + } + `, + errors: [ + { + messageId: 'noAwaitSyncQuery', + line: 2, + column: 22, + }, + ], + }) as const + ), + + // sync queries in screen with await operator are not valid + ...SYNC_QUERIES_COMBINATIONS.map( + (query) => + ({ + code: `async () => { + const element = await screen.${query}('foo') + } + `, + errors: [ + { + messageId: 'noAwaitSyncQuery', + line: 2, + column: 38, + }, + ], + }) as const + ), + + // sync queries in screen with await operator inside assert are not valid + ...SYNC_QUERIES_COMBINATIONS.map( + (query) => + ({ + code: `async () => { + expect(await screen.${query}('foo')).toBeEnabled() + } + `, + errors: [ + { + messageId: 'noAwaitSyncQuery', + line: 2, + column: 29, + }, + ], + }) as const + ), + + // sync query awaited and related to testing library module + // with custom module setting is not valid + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { screen } from '${testingFramework}' + () => { + const element = await screen.getByRole('button') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 4, column: 38 }], + }) as const + ), + // sync query awaited and related to custom module is not valid + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { screen } from 'test-utils' + () => { + const element = await screen.getByRole('button') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 4, column: 38 }], + }, + + // awaited custom sync query matching custom-queries setting is invalid + { + settings: { + 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'], + }, + code: ` + test('A valid example test', async () => { + const element = await queryByIcon('search') + }) + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 31 }], + }, + ], +}); diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-query.test.ts deleted file mode 100644 index fb01165d..00000000 --- a/tests/lib/rules/no-await-sync-query.test.ts +++ /dev/null @@ -1,263 +0,0 @@ -import rule, { RULE_NAME } from '../../../lib/rules/no-await-sync-query'; -import { - SYNC_QUERIES_COMBINATIONS, - ASYNC_QUERIES_COMBINATIONS, -} from '../../../lib/utils'; -import { createRuleTester } from '../test-utils'; - -const ruleTester = createRuleTester(); - -ruleTester.run(RULE_NAME, rule, { - valid: [ - // sync queries without await are valid - ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ - code: `() => { - const element = ${query}('foo') - } - `, - })), - // custom sync queries without await are valid - `() => { - const element = getByIcon('search') - } - `, - `() => { - const element = queryByIcon('search') - } - `, - `() => { - const element = getAllByIcon('search') - } - `, - `() => { - const element = queryAllByIcon('search') - } - `, - `async () => { - await waitFor(() => { - getByText('search'); - }); - } - `, - - // awaited custom sync query not matching custom-queries setting is valid - { - settings: { - 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'], - }, - code: ` - test('A valid example test', async () => { - const element = await getByIcon('search') - }) - `, - }, - - // sync queries without await inside assert are valid - ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ - code: `() => { - expect(${query}('foo')).toBeEnabled() - } - `, - })), - - // async queries with await operator are valid - ...ASYNC_QUERIES_COMBINATIONS.map((query) => ({ - code: `async () => { - const element = await ${query}('foo') - } - `, - })), - - // async queries with then method are valid - ...ASYNC_QUERIES_COMBINATIONS.map((query) => ({ - code: `() => { - ${query}('foo').then(() => {}); - } - `, - })), - - // sync query awaited but not related to custom module is invalid but not reported - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { screen } from 'somewhere-else' - () => { - const element = await screen.getByRole('button') - } - `, - }, - - // https://github.com/testing-library/eslint-plugin-testing-library/issues/276 - ` - // sync query within call expression but not part of the callee - const chooseElementFromSomewhere = async (text, getAllByLabelText) => { - const someElement = getAllByLabelText(text)[0].parentElement; - // ... - await someOtherAsyncFunction(); - }; - - await chooseElementFromSomewhere('someTextToUseInAQuery', getAllByLabelText); - `, - - `// edge case for coverage: - // valid use case without call expression - // so there is no innermost function scope found - await test('edge case for no innermost function scope', () => { - const foo = getAllByLabelText - }) - `, - - `// edge case for coverage: CallExpression without deepest Identifier - await someList[0](); - `, - - `// element is removed - test('movie title no longer present in DOM', async () => { - await waitForElementToBeRemoved(() => queryByText('the mummy')) - }) - `, - ], - - invalid: [ - // sync queries with await operator are not valid - ...SYNC_QUERIES_COMBINATIONS.map( - (query) => - ({ - code: `async () => { - const element = await ${query}('foo') - } - `, - errors: [ - { - messageId: 'noAwaitSyncQuery', - line: 2, - column: 31, - }, - ], - } as const) - ), - // custom sync queries with await operator are not valid - { - code: ` - async () => { - const element = await getByIcon('search') - } - `, - errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 31 }], - }, - { - code: ` - async () => { - const element = await queryByIcon('search') - } - `, - errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 31 }], - }, - { - code: ` - async () => { - const element = await screen.getAllByIcon('search') - } - `, - errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 38 }], - }, - { - code: ` - async () => { - const element = await screen.queryAllByIcon('search') - } - `, - errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 38 }], - }, - // sync queries with await operator inside assert are not valid - ...SYNC_QUERIES_COMBINATIONS.map( - (query) => - ({ - code: `async () => { - expect(await ${query}('foo')).toBeEnabled() - } - `, - errors: [ - { - messageId: 'noAwaitSyncQuery', - line: 2, - column: 22, - }, - ], - } as const) - ), - - // sync queries in screen with await operator are not valid - ...SYNC_QUERIES_COMBINATIONS.map( - (query) => - ({ - code: `async () => { - const element = await screen.${query}('foo') - } - `, - errors: [ - { - messageId: 'noAwaitSyncQuery', - line: 2, - column: 38, - }, - ], - } as const) - ), - - // sync queries in screen with await operator inside assert are not valid - ...SYNC_QUERIES_COMBINATIONS.map( - (query) => - ({ - code: `async () => { - expect(await screen.${query}('foo')).toBeEnabled() - } - `, - errors: [ - { - messageId: 'noAwaitSyncQuery', - line: 2, - column: 29, - }, - ], - } as const) - ), - - // sync query awaited and related to testing library module - // with custom module setting is not valid - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { screen } from '@testing-library/react' - () => { - const element = await screen.getByRole('button') - } - `, - errors: [{ messageId: 'noAwaitSyncQuery', line: 4, column: 38 }], - }, - // sync query awaited and related to custom module is not valid - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { screen } from 'test-utils' - () => { - const element = await screen.getByRole('button') - } - `, - errors: [{ messageId: 'noAwaitSyncQuery', line: 4, column: 38 }], - }, - - // awaited custom sync query matching custom-queries setting is invalid - { - settings: { - 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'], - }, - code: ` - test('A valid example test', async () => { - const element = await queryByIcon('search') - }) - `, - errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 31 }], - }, - ], -}); diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index c28d3071..5f3596a6 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -3,28 +3,35 @@ import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: ` + valid: [ + { + code: ` render(); screen.getByRole('button', {name: /click me/i}); `, - }, - { - code: ` + }, + { + code: ` const { container } = render(); expect(container.firstChild).toBeDefined(); `, - }, - { - code: ` + }, + { + code: ` const { container: alias } = render(); expect(alias.firstChild).toBeDefined(); `, - }, - { - code: ` + }, + { + code: ` function getExampleDOM() { const container = document.createElement('div'); container.innerHTML = \` @@ -41,177 +48,189 @@ ruleTester.run(RULE_NAME, rule, { const exampleDOM = getExampleDOM(); screen.getByText(exampleDOM, 'Print Username').click(); `, - }, - { - code: ` + }, + { + code: ` const { container: { firstChild } } = render(); expect(firstChild).toBeDefined(); `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render as renamed } from '@testing-library/react' - import { render } from 'somewhere-else' - const { container } = render(); - const button = container.querySelector('.btn-primary'); - `, - }, - { - settings: { - 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], - }, - code: ` + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as renamed } from '${testingFramework}' + import { render } from 'somewhere-else' + const { container } = render(); + const button = container.querySelector('.btn-primary'); + `, + }) as const + ), + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, + code: ` import { otherRender } from 'somewhere-else' const { container } = otherRender(); const button = container.querySelector('.btn-primary'); `, - }, - ], - invalid: [ - { - code: ` + }, + ], + invalid: [ + { + code: ` const { container } = render(); const button = container.querySelector('.btn-primary'); `, - errors: [ - { - line: 3, - column: 24, - messageId: 'noContainer', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [ + { + line: 3, + column: 24, + messageId: 'noContainer', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { render } from 'test-utils' const { container } = render(); const button = container.querySelector('.btn-primary'); `, - errors: [ - { - line: 4, - column: 24, - messageId: 'noContainer', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render as testingRender } from '@testing-library/react' + errors: [ + { + line: 4, + column: 24, + messageId: 'noContainer', + }, + ], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingRender } from '${testingFramework}' const { container: renamed } = testingRender(); const button = renamed.querySelector('.btn-primary'); `, - errors: [ - { - line: 4, - column: 24, - messageId: 'noContainer', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from '@testing-library/react' - + errors: [ + { + line: 4, + column: 24, + messageId: 'noContainer', + }, + ], + }) as const + ), + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}' + const setup = () => render() const { container } = setup() const button = container.querySelector('.btn-primary'); `, - errors: [ - { - line: 7, - column: 24, - messageId: 'noContainer', - }, - ], - }, - { - code: ` + errors: [ + { + line: 7, + column: 24, + messageId: 'noContainer', + }, + ], + }) as const + ), + { + code: ` const { container } = render(); container.querySelector(); `, - errors: [ - { - line: 3, - column: 9, - messageId: 'noContainer', - }, - ], - }, - { - code: ` + errors: [ + { + line: 3, + column: 9, + messageId: 'noContainer', + }, + ], + }, + { + code: ` const { container: alias } = render(); alias.querySelector(); `, - errors: [ - { - line: 3, - column: 9, - messageId: 'noContainer', - }, - ], - }, - { - code: ` + errors: [ + { + line: 3, + column: 9, + messageId: 'noContainer', + }, + ], + }, + { + code: ` const view = render(); const button = view.container.querySelector('.btn-primary'); `, - errors: [ - { - line: 3, - column: 29, - messageId: 'noContainer', - }, - ], - }, - { - code: ` + errors: [ + { + line: 3, + column: 29, + messageId: 'noContainer', + }, + ], + }, + { + code: ` const { container: { querySelector } } = render(); querySelector('foo'); `, - errors: [ - { - line: 3, - column: 9, - messageId: 'noContainer', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from '@testing-library/react' + errors: [ + { + line: 3, + column: 9, + messageId: 'noContainer', + }, + ], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}' const { container: { querySelector } } = render(); querySelector('foo'); `, - errors: [ - { - line: 4, - column: 9, - messageId: 'noContainer', - }, - ], - }, - { - settings: { - 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], - }, - code: ` + errors: [ + { + line: 4, + column: 9, + messageId: 'noContainer', + }, + ], + }) as const + ), + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, + code: ` const { container } = renderWithRedux(); container.querySelector(); `, - errors: [ - { - line: 3, - column: 9, - messageId: 'noContainer', - }, - ], - }, - ], + errors: [ + { + line: 3, + column: 9, + messageId: 'noContainer', + }, + ], + }, + ], }); diff --git a/tests/lib/rules/no-debugging-utils.test.ts b/tests/lib/rules/no-debugging-utils.test.ts index 56814597..7d6f1045 100644 --- a/tests/lib/rules/no-debugging-utils.test.ts +++ b/tests/lib/rules/no-debugging-utils.test.ts @@ -3,256 +3,266 @@ import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + ruleTester.run(RULE_NAME, rule, { - valid: [ - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `debug()`, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + valid: [ + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `debug()`, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { screen } from 'somewhere-else' screen.debug() `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `() => { + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `() => { const somethingElse = {} const { debug } = foo() debug() }`, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` let foo const debug = require('debug') debug() `, - }, - { - code: ` + }, + { + code: ` const { test } = render() test() `, - }, - { - code: ` + }, + { + code: ` const utils = render() utils.debug `, - }, - { - code: ` + }, + { + code: ` const utils = render() utils.foo() `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `screen.debug()`, - }, - { - code: `console.debug()`, - }, - { - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `screen.debug()`, + }, + { + code: `console.debug()`, + }, + { + code: ` const consoleDebug = console.debug consoleDebug() `, - }, - { - code: ` + }, + { + code: ` const { debug } = console debug() `, - }, - { - code: ` + }, + { + code: ` const { debug: consoleDebug } = console consoleDebug() `, - }, - { - code: ` + }, + { + code: ` const { screen } = require('@testing-library/dom') screen.debug `, - }, - { - code: ` + }, + { + code: ` import { screen } from '@testing-library/dom' screen.debug `, - }, - { - code: ` + }, + { + code: ` import { screen } from '@testing-library/dom' screen.logTestingPlaygroundURL() `, - options: [{ utilsToCheckFor: { logTestingPlaygroundURL: false } }], - }, - { - code: ` + options: [{ utilsToCheckFor: { logTestingPlaygroundURL: false } }], + }, + { + code: ` import { screen } from '@testing-library/dom' screen.logTestingPlaygroundURL() `, - options: [{ utilsToCheckFor: undefined }], - }, - { - code: `const { queries } = require('@testing-library/dom')`, - }, - { - code: `import * as dtl from '@testing-library/dom'; + options: [{ utilsToCheckFor: undefined }], + }, + { + code: `const { queries } = require('@testing-library/dom')`, + }, + { + code: `import * as dtl from '@testing-library/dom'; const foo = dtl.debug; `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import * as foo from '@somewhere/else'; foo.debug(); `, - }, - { - code: `import { queries } from '@testing-library/dom'`, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + code: `import { queries } from '@testing-library/dom'`, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` const { screen } = require('something-else') screen.debug() `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { screen } from 'something-else' screen.debug() `, - }, - { - code: ` + }, + { + code: ` async function foo() { const foo = await bar; } `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { debug as testingDebug } from 'test-utils' import { debug } from 'somewhere-else' debug() `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render as testingRender } from '@testing-library/react' + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map((testingFramework) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingRender } from '${testingFramework}' import { render } from 'somewhere-else' - + const { debug } = render(element) - + somethingElse() debug() `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render as testingRender } from '@testing-library/react' + })), + ...SUPPORTED_TESTING_FRAMEWORKS.map((testingFramework) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingRender } from '${testingFramework}' import { render } from 'somewhere-else' - + const { debug } = render(element) const { debug: testingDebug } = testingRender(element) - + somethingElse() debug() `, - }, + })), - `// cover edge case for https://github.com/testing-library/eslint-plugin-testing-library/issues/306 - thing.method.lastCall.args[0](); - `, - ], + { + code: ` + // cover edge case for https://github.com/testing-library/eslint-plugin-testing-library/issues/306 + thing.method.lastCall.args[0](); + `, + }, + ], - invalid: [ - { - code: `debug()`, - errors: [{ line: 1, column: 1, messageId: 'noDebug' }], - }, - { - code: ` + invalid: [ + { + code: `debug()`, + errors: [{ line: 1, column: 1, messageId: 'noDebug' }], + }, + { + code: ` import { screen } from 'aggressive-reporting' screen.debug() `, - errors: [{ line: 3, column: 14, messageId: 'noDebug' }], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [{ line: 3, column: 14, messageId: 'noDebug' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { screen } from 'test-utils' screen.debug() `, - errors: [{ line: 3, column: 14, messageId: 'noDebug' }], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [{ line: 3, column: 14, messageId: 'noDebug' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { debug as testingDebug } from 'test-utils' testingDebug() `, - errors: [{ line: 3, column: 7, messageId: 'noDebug' }], - }, - { - code: ` + errors: [{ line: 3, column: 7, messageId: 'noDebug' }], + }, + { + code: ` const { debug } = render() debug() `, - errors: [ - { - line: 3, - column: 9, - messageId: 'noDebug', - }, - ], - }, - { - settings: { - 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], - }, - code: ` + errors: [ + { + line: 3, + column: 9, + messageId: 'noDebug', + }, + ], + }, + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, + code: ` const { debug } = renderWithRedux() debug() `, - errors: [ - { - line: 3, - column: 9, - messageId: 'noDebug', - }, - ], - }, - { - code: ` + errors: [ + { + line: 3, + column: 9, + messageId: 'noDebug', + }, + ], + }, + { + code: ` const utils = render() utils.debug() `, - errors: [ - { - line: 3, - column: 15, - messageId: 'noDebug', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [ + { + line: 3, + column: 15, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { render } from 'test-utils' const setup = () => render() @@ -260,90 +270,90 @@ ruleTester.run(RULE_NAME, rule, { const utils = setup() utils.debug() `, - errors: [ - { - line: 7, - column: 15, - messageId: 'noDebug', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// aggressive reporting disabled + errors: [ + { + line: 7, + column: 15, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled import { render } from 'test-utils' const utils = render() utils.debug() `, - errors: [ - { - line: 4, - column: 15, - messageId: 'noDebug', - }, - ], - }, - { - code: ` + errors: [ + { + line: 4, + column: 15, + messageId: 'noDebug', + }, + ], + }, + { + code: ` const utils = render() utils.debug() utils.foo() utils.debug() `, - errors: [ - { - line: 3, - column: 15, - messageId: 'noDebug', - }, - { - line: 5, - column: 15, - messageId: 'noDebug', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// aggressive reporting disabled + errors: [ + { + line: 3, + column: 15, + messageId: 'noDebug', + }, + { + line: 5, + column: 15, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled import { render } from 'test-utils' const utils = render() utils.debug() utils.foo() utils.debug() `, - errors: [ - { - line: 4, - column: 15, - messageId: 'noDebug', - }, - { - line: 6, - column: 15, - messageId: 'noDebug', - }, - ], - }, - { - code: ` + errors: [ + { + line: 4, + column: 15, + messageId: 'noDebug', + }, + { + line: 6, + column: 15, + messageId: 'noDebug', + }, + ], + }, + { + code: ` describe(() => { test(async () => { const { debug } = await render("foo") debug() }) })`, - errors: [ - { - line: 5, - column: 11, - messageId: 'noDebug', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// aggressive reporting disabled + errors: [ + { + line: 5, + column: 11, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled import { render } from 'test-utils' describe(() => { test(async () => { @@ -351,33 +361,33 @@ ruleTester.run(RULE_NAME, rule, { debug() }) })`, - errors: [ - { - line: 6, - column: 11, - messageId: 'noDebug', - }, - ], - }, - { - code: ` + errors: [ + { + line: 6, + column: 11, + messageId: 'noDebug', + }, + ], + }, + { + code: ` describe(() => { test(async () => { const utils = await render("foo") utils.debug() }) })`, - errors: [ - { - line: 5, - column: 17, - messageId: 'noDebug', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// aggressive reporting disabled + errors: [ + { + line: 5, + column: 17, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled import { render } from 'test-utils' describe(() => { test(async () => { @@ -385,260 +395,225 @@ ruleTester.run(RULE_NAME, rule, { utils.debug() }) })`, - errors: [ - { - line: 6, - column: 17, - messageId: 'noDebug', - }, - ], - }, - { - code: ` + errors: [ + { + line: 6, + column: 17, + messageId: 'noDebug', + }, + ], + }, + { + code: ` const { screen } = require('@testing-library/dom') screen.debug() `, - errors: [ - { - line: 3, - column: 16, - messageId: 'noDebug', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// aggressive reporting disabled + errors: [ + { + line: 3, + column: 16, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled const { screen } = require('@testing-library/dom') screen.debug() `, - errors: [ - { - line: 3, - column: 16, - messageId: 'noDebug', - }, - ], - }, - { - code: ` + errors: [ + { + line: 3, + column: 16, + messageId: 'noDebug', + }, + ], + }, + { + code: ` import { screen } from '@testing-library/dom' screen.debug() `, - errors: [ - { - line: 3, - column: 16, - messageId: 'noDebug', - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/dom' - screen.logTestingPlaygroundURL() - `, - options: [{ utilsToCheckFor: { logTestingPlaygroundURL: true } }], - errors: [ - { - line: 3, - column: 16, - messageId: 'noDebug', - }, - ], - }, - { - code: ` + errors: [ + { + line: 3, + column: 16, + messageId: 'noDebug', + }, + ], + }, + { + code: ` import { logRoles } from '@testing-library/dom' logRoles(document.createElement('nav')) `, - options: [{ utilsToCheckFor: { logRoles: true } }], - errors: [ - { - line: 3, - column: 9, - messageId: 'noDebug', - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/dom' - screen.logTestingPlaygroundURL() - `, - options: [{ utilsToCheckFor: { logRoles: true } }], - errors: [ - { - line: 3, - column: 16, - messageId: 'noDebug', - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/dom' - screen.logTestingPlaygroundURL() - `, - options: [{ utilsToCheckFor: { debug: false } }], - errors: [ - { - line: 3, - column: 16, - messageId: 'noDebug', - }, - ], - }, - { - code: ` + errors: [ + { + line: 3, + column: 9, + messageId: 'noDebug', + }, + ], + }, + { + code: ` import { screen } from '@testing-library/dom' screen.logTestingPlaygroundURL() `, - options: [{ utilsToCheckFor: {} }], - errors: [ - { - line: 3, - column: 16, - messageId: 'noDebug', - }, - ], - }, - { - code: ` + errors: [ + { + line: 3, + column: 16, + messageId: 'noDebug', + }, + ], + }, + { + code: ` import { screen } from '@testing-library/dom' screen.logTestingPlaygroundURL() `, - errors: [ - { - line: 3, - column: 16, - messageId: 'noDebug', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// aggressive reporting disabled + options: [{ utilsToCheckFor: {} }], + errors: [ + { + line: 3, + column: 16, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled import { screen } from '@testing-library/dom' screen.debug() `, - errors: [ - { - line: 3, - column: 16, - messageId: 'noDebug', - }, - ], - }, - { - // https://github.com/testing-library/eslint-plugin-testing-library/issues/174 - code: ` + errors: [ + { + line: 3, + column: 16, + messageId: 'noDebug', + }, + ], + }, + { + // https://github.com/testing-library/eslint-plugin-testing-library/issues/174 + code: ` import { screen, render } from '@testing-library/dom' screen.debug() `, - errors: [ - { - line: 3, - column: 16, - messageId: 'noDebug', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// aggressive reporting disabled + errors: [ + { + line: 3, + column: 16, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled import { screen, render } from '@testing-library/dom' screen.debug() `, - errors: [ - { - line: 3, - column: 16, - messageId: 'noDebug', - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [ + { + line: 3, + column: 16, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import * as dtl from '@testing-library/dom'; dtl.debug(); `, - errors: [ - { - messageId: 'noDebug', - line: 3, - column: 13, - }, - ], - }, - { - code: ` + errors: [ + { + messageId: 'noDebug', + line: 3, + column: 13, + }, + ], + }, + { + code: ` import { render } from 'aggressive-reporting' - + const { debug } = render(element) - + somethingElse() debug() `, - errors: [{ line: 7, column: 7, messageId: 'noDebug' }], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from '@testing-library/react' - + errors: [{ line: 7, column: 7, messageId: 'noDebug' }], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}' + const { debug } = render(element) - + somethingElse() debug() `, - errors: [{ line: 7, column: 7, messageId: 'noDebug' }], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [{ line: 7, column: 7, messageId: 'noDebug' }], + }) as const + ), + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { render } from 'test-utils' - + const { debug: renamed } = render(element) - + somethingElse() renamed() `, - errors: [{ line: 7, column: 7, messageId: 'noDebug' }], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from '@testing-library/react' - + errors: [{ line: 7, column: 7, messageId: 'noDebug' }], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}' + const utils = render(element) - + somethingElse() utils.debug() `, - errors: [{ line: 7, column: 13, messageId: 'noDebug' }], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - 'testing-library/custom-renders': ['testingRender'], - }, - code: `// aggressive reporting disabled, custom render set + errors: [{ line: 7, column: 13, messageId: 'noDebug' }], + }) as const + ), + { + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['testingRender'], + }, + code: `// aggressive reporting disabled, custom render set import { testingRender } from 'test-utils' - + const { debug: renamedDebug } = testingRender(element) - + somethingElse() renamedDebug() `, - errors: [{ line: 7, column: 7, messageId: 'noDebug' }], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from '@testing-library/react' - + errors: [{ line: 7, column: 7, messageId: 'noDebug' }], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}' + const utils = render(element) const { debug: renamedDestructuredDebug } = console const { debug } = console @@ -650,7 +625,8 @@ ruleTester.run(RULE_NAME, rule, { utils.debug() renamedDestructuredDebug('foo') `, - errors: [{ line: 12, column: 13, messageId: 'noDebug' }], - }, - ], + errors: [{ line: 12, column: 13, messageId: 'noDebug' }], + }) as const + ), + ], }); diff --git a/tests/lib/rules/no-dom-import.test.ts b/tests/lib/rules/no-dom-import.test.ts index 4dfc0623..e5fbce0c 100644 --- a/tests/lib/rules/no-dom-import.test.ts +++ b/tests/lib/rules/no-dom-import.test.ts @@ -3,229 +3,231 @@ import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +const SUPPORTED_TESTING_FRAMEWORKS = [ + { + configOption: 'angular', + oldName: '@testing-library/angular', + newName: '@testing-library/angular', + }, + { + configOption: 'react', + oldName: 'react-testing-library', + newName: '@testing-library/react', + }, + { + configOption: 'vue', + oldName: 'vue-testing-library', + newName: '@testing-library/vue', + }, + { + configOption: 'marko', + oldName: '@marko/testing-library', + newName: '@marko/testing-library', + }, +]; + ruleTester.run(RULE_NAME, rule, { - valid: [ - 'import { foo } from "foo"', - 'import "foo"', - 'import { fireEvent } from "react-testing-library"', - 'import * as testing from "react-testing-library"', - 'import { fireEvent } from "@testing-library/react"', - 'import * as testing from "@testing-library/react"', - 'import "react-testing-library"', - 'import "@testing-library/react"', - 'const { foo } = require("foo")', - 'require("foo")', - 'require("")', - 'require()', - 'const { fireEvent } = require("react-testing-library")', - 'const { fireEvent } = require("@testing-library/react")', - 'require("react-testing-library")', - 'require("@testing-library/react")', - { - code: 'import { fireEvent } from "test-utils"', - settings: { 'testing-library/utils-module': 'test-utils' }, - }, - ], - invalid: [ - { - code: 'import { fireEvent } from "dom-testing-library"', - errors: [ - { - messageId: 'noDomImport', - }, - ], - output: 'import { fireEvent } from "dom-testing-library"', - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: dom-testing-library imported with custom module setting - import { fireEvent } from "dom-testing-library"`, - errors: [ - { - line: 3, - messageId: 'noDomImport', - }, - ], - output: ` + valid: [ + 'import { foo } from "foo"', + 'import "foo"', + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap(({ oldName, newName }) => + [oldName, newName !== oldName ? newName : null].flatMap( + (testingFramework) => { + if (!testingFramework) { + return []; + } + + return [ + `import { fireEvent } from "${testingFramework}"`, + `import * as testing from "${testingFramework}"`, + `import "${testingFramework}"`, + ]; + } + ) + ), + 'const { foo } = require("foo")', + 'require("foo")', + 'require("")', + 'require()', + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap(({ oldName, newName }) => + [oldName, newName !== oldName ? newName : null].flatMap( + (testingFramework) => { + if (!testingFramework) { + return []; + } + + return [ + `const { fireEvent } = require("${testingFramework}")`, + `const { fireEvent: testing } = require("${testingFramework}")`, + `require("${testingFramework}")`, + ]; + } + ) + ), + { + code: 'import { fireEvent } from "test-utils"', + settings: { 'testing-library/utils-module': 'test-utils' }, + }, + ], + invalid: [ + { + code: 'import { fireEvent } from "dom-testing-library"', + errors: [ + { + messageId: 'noDomImport', + }, + ], + output: null, + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: dom-testing-library imported with custom module setting import { fireEvent } from "dom-testing-library"`, - }, - { - code: 'import { fireEvent } from "dom-testing-library"', - options: ['react'], - errors: [ - { - messageId: 'noDomImportFramework', - data: { - module: 'react-testing-library', - }, - }, - ], - output: `import { fireEvent } from "react-testing-library"`, - }, - // Single quote or double quotes should not be replaced - { - code: `import { fireEvent } from 'dom-testing-library'`, - options: ['react'], - errors: [ - { - messageId: 'noDomImportFramework', - data: { - module: 'react-testing-library', - }, - }, - ], - output: `import { fireEvent } from 'react-testing-library'`, - }, - { - code: 'import * as testing from "dom-testing-library"', - errors: [ - { - messageId: 'noDomImport', - }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + line: 3, + messageId: 'noDomImport', + }, + ], + output: null, + }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap( + ({ configOption, oldName, newName }) => + [true, false].flatMap((isOldImport) => + // Single quote or double quotes should not be replaced + [`'`, `"`].flatMap((quote) => [ + { + code: `const { fireEvent } = require(${quote}${ + isOldImport ? 'dom-testing-library' : '@testing-library/dom' + }${quote})`, + options: [configOption], + errors: [ + { + data: { module: isOldImport ? oldName : newName }, + messageId: 'noDomImportFramework', + }, + ], + output: `const { fireEvent } = require(${quote}${ + isOldImport ? oldName : newName + }${quote})`, + } as const, + { + code: `import { fireEvent } from ${quote}${ + isOldImport ? 'dom-testing-library' : '@testing-library/dom' + }${quote}`, + options: [configOption], + errors: [ + { + data: { module: isOldImport ? oldName : newName }, + messageId: 'noDomImportFramework', + }, + ], + output: `import { fireEvent } from ${quote}${ + isOldImport ? oldName : newName + }${quote}`, + } as const, + ]) + ) + ), + { + code: 'import * as testing from "dom-testing-library"', + errors: [{ messageId: 'noDomImport' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` // case: dom-testing-library wildcard imported with custom module setting import * as testing from "dom-testing-library"`, - errors: [ - { - line: 3, - messageId: 'noDomImport', - }, - ], - }, - { - code: 'import { fireEvent } from "@testing-library/dom"', - errors: [ - { - messageId: 'noDomImport', - }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ line: 3, messageId: 'noDomImport' }], + }, + { + code: 'import { fireEvent } from "@testing-library/dom"', + errors: [{ messageId: 'noDomImport' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` // case: @testing-library/dom imported with custom module setting import { fireEvent } from "@testing-library/dom"`, - errors: [ - { - line: 3, - messageId: 'noDomImport', - }, - ], - }, - { - code: 'import * as testing from "@testing-library/dom"', - errors: [ - { - messageId: 'noDomImport', - }, - ], - }, - { - code: 'import "dom-testing-library"', - errors: [ - { - messageId: 'noDomImport', - }, - ], - }, - { - code: 'import "@testing-library/dom"', - errors: [ - { - messageId: 'noDomImport', - }, - ], - }, - { - code: 'const { fireEvent } = require("dom-testing-library")', - errors: [ - { - messageId: 'noDomImport', - }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ line: 3, messageId: 'noDomImport' }], + }, + { + code: 'import * as testing from "@testing-library/dom"', + errors: [{ messageId: 'noDomImport' }], + }, + { + code: 'import "dom-testing-library"', + errors: [{ messageId: 'noDomImport' }], + }, + { + code: 'import "@testing-library/dom"', + errors: [{ messageId: 'noDomImport' }], + }, + { + code: 'const { fireEvent } = require("dom-testing-library")', + errors: [{ messageId: 'noDomImport' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` // case: dom-testing-library required with custom module setting const { fireEvent } = require("dom-testing-library")`, - errors: [ - { - line: 3, - messageId: 'noDomImport', - }, - ], - }, - { - code: 'const { fireEvent } = require("@testing-library/dom")', - errors: [ - { - messageId: 'noDomImport', - }, - ], - }, - { - code: 'const { fireEvent } = require("@testing-library/dom")', - options: ['vue'], - errors: [ - { - messageId: 'noDomImportFramework', - data: { - module: '@testing-library/vue', - }, - }, - ], - output: 'const { fireEvent } = require("@testing-library/vue")', - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: @testing-library/dom required with custom module setting - const { fireEvent } = require("@testing-library/dom")`, - options: ['vue'], - errors: [ - { - messageId: 'noDomImportFramework', - data: { - module: '@testing-library/vue', - }, - }, - ], - output: ` - // case: @testing-library/dom required with custom module setting - const { fireEvent } = require("@testing-library/vue")`, - }, - { - code: 'require("dom-testing-library")', - errors: [ - { - messageId: 'noDomImport', - }, - ], - }, - { - code: 'require("@testing-library/dom")', - errors: [ - { - messageId: 'noDomImport', - }, - ], - }, - ], + errors: [{ line: 3, messageId: 'noDomImport' }], + }, + { + code: 'const { fireEvent } = require("@testing-library/dom")', + errors: [{ messageId: 'noDomImport' }], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap( + ({ configOption, oldName, newName }) => + [true, false].map( + (isOldImport) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + // case: @testing-library/dom required with custom module setting + const { fireEvent } = require("${ + isOldImport ? 'dom-testing-library' : '@testing-library/dom' + }") + `, + options: [configOption], + errors: [ + { + data: { module: isOldImport ? oldName : newName }, + messageId: 'noDomImportFramework', + }, + ], + output: ` + // case: @testing-library/dom required with custom module setting + const { fireEvent } = require("${ + isOldImport ? oldName : newName + }") + `, + }) as const + ) + ), + { + code: 'require("dom-testing-library")', + errors: [{ messageId: 'noDomImport' }], + }, + { + code: 'require("@testing-library/dom")', + errors: [{ messageId: 'noDomImport' }], + }, + { + code: ` + require("@testing-library/dom"); + require("@testing-library/react");`, + errors: [{ line: 2, messageId: 'noDomImport' }], + }, + { + code: ` + import { render } from '@testing-library/react'; + import { screen } from '@testing-library/dom';`, + errors: [{ line: 3, messageId: 'noDomImport' }], + }, + ], }); diff --git a/tests/lib/rules/no-global-regexp-flag-in-query.test.ts b/tests/lib/rules/no-global-regexp-flag-in-query.test.ts new file mode 100644 index 00000000..f885771d --- /dev/null +++ b/tests/lib/rules/no-global-regexp-flag-in-query.test.ts @@ -0,0 +1,224 @@ +import rule, { + RULE_NAME, +} from '../../../lib/rules/no-global-regexp-flag-in-query'; +import { createRuleTester } from '../test-utils'; + +const ruleTester = createRuleTester(); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + ` + import { screen } from '@testing-library/dom' + screen.getByText(/hello/) + `, + ` + import { screen } from '@testing-library/dom' + screen.getByText(/hello/i) + `, + ` + import { screen } from '@testing-library/dom' + screen.getByText('hello') + `, + + ` + import { screen } from '@testing-library/dom' + screen.findByRole('button', {name: /hello/}) + `, + ` + import { screen } from '@testing-library/dom' + screen.findByRole('button', {name: /hello/im}) + `, + ` + import { screen } from '@testing-library/dom' + screen.findByRole('button', {name: 'hello'}) + `, + ` + const utils = render() + utils.findByRole('button', {name: /hello/m}) + `, + ` + const {queryAllByPlaceholderText} = render() + queryAllByPlaceholderText(/hello/i) + `, + ` + import { within } from '@testing-library/dom' + within(element).findByRole('button', {name: /hello/i}) + `, + ` + import { within } from '@testing-library/dom' + within(element).queryByText('Hello') + `, + ` + const text = 'hello'; + /hello/g.test(text) + text.match(/hello/g) + `, + ` + const text = somethingElse() + /hello/g.test(text) + text.match(/hello/g) + `, + ` + import somethingElse from 'somethingElse' + somethingElse.lookup(/hello/g) + `, + ` + import { screen } from '@testing-library/dom' + screen.notAQuery(/hello/g) + `, + ` + import { screen } from '@testing-library/dom' + screen.notAQuery('button', {name: /hello/g}) + `, + ` + const utils = render() + utils.notAQuery('button', {name: /hello/i}) + `, + ` + const utils = render() + utils.notAQuery(/hello/i) + `, + + // issue #565 + ` + import { screen } from "@testing-library/react" + + describe("App", () => { + test("is rendered", async () => { + await screen.findByText("Hello World", { exact: false }); + }) + }) + `, + ], + invalid: [ + { + code: ` + import { screen } from '@testing-library/dom' + screen.getByText(/hello/g)`, + errors: [ + { + messageId: 'noGlobalRegExpFlagInQuery', + line: 3, + column: 26, + }, + ], + output: ` + import { screen } from '@testing-library/dom' + screen.getByText(/hello/)`, + }, + { + code: ` + import { screen } from '@testing-library/dom' + screen.findByRole('button', {name: /hellogg/g})`, + errors: [ + { + messageId: 'noGlobalRegExpFlagInQuery', + line: 3, + column: 44, + }, + ], + output: ` + import { screen } from '@testing-library/dom' + screen.findByRole('button', {name: /hellogg/})`, + }, + { + code: ` + import { screen } from '@testing-library/dom' + screen.findByRole('button', {otherProp: true, name: /hello/g})`, + errors: [ + { + messageId: 'noGlobalRegExpFlagInQuery', + line: 3, + column: 61, + }, + ], + output: ` + import { screen } from '@testing-library/dom' + screen.findByRole('button', {otherProp: true, name: /hello/})`, + }, + { + code: ` + const utils = render() + utils.findByRole('button', {name: /hello/ig})`, + errors: [ + { + messageId: 'noGlobalRegExpFlagInQuery', + line: 3, + column: 43, + }, + ], + output: ` + const utils = render() + utils.findByRole('button', {name: /hello/i})`, + }, + { + code: ` + const {queryAllByLabelText} = render() + queryAllByLabelText(/hello/gi)`, + errors: [ + { + messageId: 'noGlobalRegExpFlagInQuery', + line: 3, + column: 29, + }, + ], + output: ` + const {queryAllByLabelText} = render() + queryAllByLabelText(/hello/i)`, + }, + { + code: ` + import { within } from '@testing-library/dom' + within(element).findByRole('button', {name: /hello/igm})`, + errors: [ + { + messageId: 'noGlobalRegExpFlagInQuery', + line: 3, + column: 53, + }, + ], + output: ` + import { within } from '@testing-library/dom' + within(element).findByRole('button', {name: /hello/im})`, + }, + { + code: ` + import { within } from '@testing-library/dom' + within(element).queryAllByText(/hello/ig)`, + errors: [ + { + messageId: 'noGlobalRegExpFlagInQuery', + line: 3, + column: 40, + }, + ], + output: ` + import { within } from '@testing-library/dom' + within(element).queryAllByText(/hello/i)`, + }, + { + code: ` + const countRegExp = /count/gm + const anotherRegExp = /something/mgi + expect(screen.getByText(countRegExp)).toBeInTheDocument() + expect(screen.getByAllText(anotherRegExp)).toBeInTheDocument()`, + errors: [ + { + messageId: 'noGlobalRegExpFlagInQuery', + line: 4, + column: 28, + }, + { + messageId: 'noGlobalRegExpFlagInQuery', + line: 5, + column: 31, + }, + ], + output: ` + const countRegExp = /count/m + const anotherRegExp = /something/mi + expect(screen.getByText(countRegExp)).toBeInTheDocument() + expect(screen.getByAllText(anotherRegExp)).toBeInTheDocument()`, + }, + ], +}); diff --git a/tests/lib/rules/no-manual-cleanup.test.ts b/tests/lib/rules/no-manual-cleanup.test.ts index 73732736..88fc87f6 100644 --- a/tests/lib/rules/no-manual-cleanup.test.ts +++ b/tests/lib/rules/no-manual-cleanup.test.ts @@ -4,236 +4,304 @@ import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); const ALL_TESTING_LIBRARIES_WITH_CLEANUP = [ - '@testing-library/preact', - '@testing-library/react', - '@testing-library/svelte', - '@testing-library/vue', - '@marko/testing-library', + '@testing-library/preact', + '@testing-library/react', + '@testing-library/svelte', + '@testing-library/vue', + '@marko/testing-library', ]; ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: `import "@testing-library/react"`, - }, - { - code: `import { cleanup } from "test-utils"`, - }, - { - // Angular Testing Library doesn't have `cleanup` util - code: `import { cleanup } from "@testing-library/angular"`, - }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: `import { render } from "${lib}"`, - })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: `import utils from "${lib}"`, - })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: ` + valid: [ + { + code: `import "@testing-library/react"`, + }, + { + code: `import { cleanup } from "test-utils"`, + }, + { + // Angular Testing Library doesn't have `cleanup` util + code: `import { cleanup } from "@testing-library/angular"`, + }, + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ + code: `import { render } from "${lib}"`, + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ + code: `import utils from "${lib}"`, + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ + code: ` import utils from "${lib}" utils.render() `, - })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: `const { render, within } = require("${lib}")`, - })), - { - code: `const { cleanup } = require("any-other-library")`, - }, - { - code: ` + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ + code: `const { render, within } = require("${lib}")`, + })), + { + code: `const { cleanup } = require("any-other-library")`, + }, + { + code: ` const utils = require("any-other-library") utils.cleanup() `, - }, - { - // For test coverage - code: `const utils = render("something")`, - }, - { - code: `const utils = require(moduleName)`, - }, - ], - invalid: [ - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( - (lib) => - ({ - code: `import { render, cleanup } from "${lib}"`, - errors: [ - { - line: 1, - column: 18, // error points to `cleanup` - messageId: 'noManualCleanup', - }, - ], - } as const) - ), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( - (lib) => - ({ - // official testing-library packages should be reported with custom module setting - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { cleanup, render } from "${lib}"`, - errors: [ - { - line: 1, - column: 10, // error points to `cleanup` - messageId: 'noManualCleanup', - }, - ], - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + // For test coverage + code: `const utils = render("something")`, + }, + { + code: `const utils = require(moduleName)`, + }, + ], + invalid: [ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: `import { render, cleanup } from "${lib}"`, + errors: [ + { + line: 1, + column: 18, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: ` + import { render, cleanup } from "${lib}" + import userEvent from "@testing-library/user-event" + `, + errors: [ + { + line: 2, + column: 24, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: ` + import userEvent from "@testing-library/user-event" + import { render, cleanup } from "${lib}" + `, + errors: [ + { + line: 3, + column: 24, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + // official testing-library packages should be reported with custom module setting + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `import { cleanup, render } from "${lib}"`, + errors: [ + { + line: 1, + column: 10, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { render, cleanup } from 'test-utils' `, - errors: [{ line: 2, column: 26, messageId: 'noManualCleanup' }], - }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( - (lib) => - ({ - code: `import { cleanup as myCustomCleanup } from "${lib}"`, - errors: [ - { - line: 1, - column: 10, // error points to `cleanup` - messageId: 'noManualCleanup', - }, - ], - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ line: 2, column: 26, messageId: 'noManualCleanup' }], + }, + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: `import { cleanup as myCustomCleanup } from "${lib}"`, + errors: [ + { + line: 1, + column: 10, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { cleanup as myCustomCleanup } from 'test-utils' `, - errors: [{ line: 2, column: 18, messageId: 'noManualCleanup' }], - }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( - (lib) => - ({ - code: `import utils, { cleanup } from "${lib}"`, - errors: [ - { - line: 1, - column: 17, // error points to `cleanup` - messageId: 'noManualCleanup', - }, - ], - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ line: 2, column: 18, messageId: 'noManualCleanup' }], + }, + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: `import utils, { cleanup } from "${lib}"`, + errors: [ + { + line: 1, + column: 17, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import utils, { cleanup } from 'test-utils' `, - errors: [{ line: 2, column: 25, messageId: 'noManualCleanup' }], - }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( - (lib) => - ({ - code: ` + errors: [{ line: 2, column: 25, messageId: 'noManualCleanup' }], + }, + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: ` import utils from "${lib}" afterEach(() => utils.cleanup()) `, - errors: [ - { - line: 3, - column: 31, - messageId: 'noManualCleanup', - }, - ], - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + line: 3, + column: 31, + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import utils from 'test-utils' afterEach(() => utils.cleanup()) `, - errors: [{ line: 3, column: 31, messageId: 'noManualCleanup' }], - }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( - (lib) => - ({ - code: ` + errors: [{ line: 3, column: 31, messageId: 'noManualCleanup' }], + }, + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: ` import utils from "${lib}" afterEach(utils.cleanup) `, - errors: [ - { - line: 3, - column: 25, - messageId: 'noManualCleanup', - }, - ], - } as const) - ), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( - (lib) => - ({ - code: `const { cleanup } = require("${lib}")`, - errors: [ - { - line: 1, - column: 9, // error points to `cleanup` - messageId: 'noManualCleanup', - }, - ], - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + line: 3, + column: 25, + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: `const { cleanup } = require("${lib}")`, + errors: [ + { + line: 1, + column: 9, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` const { render, cleanup } = require('test-utils') `, - errors: [{ line: 2, column: 25, messageId: 'noManualCleanup' }], - }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( - (lib) => - ({ - code: ` + errors: [{ line: 2, column: 25, messageId: 'noManualCleanup' }], + }, + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: ` const utils = require("${lib}") afterEach(() => utils.cleanup()) `, - errors: [ - { - line: 3, - column: 31, - messageId: 'noManualCleanup', - }, - ], - } as const) - ), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( - (lib) => - ({ - code: ` + errors: [ + { + line: 3, + column: 31, + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: ` const utils = require("${lib}") afterEach(utils.cleanup) `, - errors: [ - { - line: 3, - column: 25, - messageId: 'noManualCleanup', - }, - ], - } as const) - ), - ], + errors: [ + { + line: 3, + column: 25, + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: ` + import { render } from "${lib}"; + import { cleanup } from "${lib}"; + afterEach(cleanup); + `, + errors: [ + { + line: 3, + column: 18, + messageId: 'noManualCleanup', + }, + ], + }) as const + ), + { + code: ` + import { cleanup as cleanupVue } from "@testing-library/vue"; + import { cleanup as cleanupReact } from "@testing-library/react"; + afterEach(() => { cleanupVue(); cleanupReact(); }); + `, + errors: [ + { + line: 2, + column: 14, + messageId: 'noManualCleanup', + }, + { + line: 3, + column: 14, + messageId: 'noManualCleanup', + }, + ], + }, + ], }); diff --git a/tests/lib/rules/no-node-access.test.ts b/tests/lib/rules/no-node-access.test.ts index 39d172a4..41c29b16 100644 --- a/tests/lib/rules/no-node-access.test.ts +++ b/tests/lib/rules/no-node-access.test.ts @@ -1,55 +1,73 @@ -import rule, { RULE_NAME } from '../../../lib/rules/no-node-access'; +import { InvalidTestCase, ValidTestCase } from '@typescript-eslint/rule-tester'; + +import rule, { + RULE_NAME, + Options, + MessageIds, +} from '../../../lib/rules/no-node-access'; +import { EVENT_HANDLER_METHODS } from '../../../lib/utils'; import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +type RuleValidTestCase = ValidTestCase; +type RuleInvalidTestCase = InvalidTestCase; + +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: ` - import { screen } from '@testing-library/react'; - + valid: SUPPORTED_TESTING_FRAMEWORKS.flatMap( + (testingFramework) => [ + { + code: ` + import { screen } from '${testingFramework}'; + const buttonText = screen.getByText('submit'); `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; const { getByText } = screen const firstChild = getByText('submit'); expect(firstChild).toBeInTheDocument() `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; const firstChild = screen.getByText('submit'); expect(firstChild).toBeInTheDocument() `, - }, - { - code: ` - import { screen } from '@testing-library/react'; - + }, + { + code: ` + import { screen } from '${testingFramework}'; + const { getByText } = screen; const button = getByRole('button'); expect(button).toHaveTextContent('submit'); `, - }, - { - code: ` - import { render, within } from '@testing-library/react'; + }, + { + code: ` + import { render, within } from '${testingFramework}'; const { getByLabelText } = render(); const signInModal = getByLabelText('Sign In'); within(signInModal).getByPlaceholderText('Username'); `, - }, - { - code: ` - // case: code not related to testing library at all + }, + { + code: ` + // case: code not related to ${testingFramework} at all ReactDOM.render( @@ -63,171 +81,414 @@ ruleTester.run(RULE_NAME, rule, { document.getElementById('root') ); `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: custom module set but not imported (aggressive reporting limited) + }, + { + code: `// issue #386 examples, props.children should not be reported + import { screen } from '${testingFramework}'; + jest.mock('@/some/path', () => ({ + someProperty: jest.fn((props) => props.children), + })); + `, + }, + { + code: `// issue #386 examples + import { screen } from '${testingFramework}'; + function ComponentA(props) { + if (props.children) { + // ... + } + + return
{props.children}
+ } + `, + }, + { + code: `/* related to issue #386 fix + * now all node accessing properties (listed in lib/utils/index.ts, in PROPERTIES_RETURNING_NODES) + * will not be reported by this rule because anything props.something won't be reported. + */ + import { screen } from '${testingFramework}'; + function ComponentA(props) { + if (props.firstChild) { + // ... + } + + return
{props.nextSibling}
+ } + `, + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case: custom module set but not imported using ${testingFramework} (aggressive reporting limited) const closestButton = document.getElementById('submit-btn').closest('button'); expect(closestButton).toBeInTheDocument(); `, - }, - { - code: ` - // case: without importing TL (aggressive reporting skipped) + }, + { + code: ` + // case: without importing ${testingFramework} (aggressive reporting skipped) const closestButton = document.getElementById('submit-btn') expect(closestButton).toBeInTheDocument(); `, - }, - ], - invalid: [ - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: importing from custom module (aggressive reporting limited) + }, + { + options: [{ allowContainerFirstChild: true }], + code: ` + import { render } from '${testingFramework}'; + + const { container } = render() + + expect(container.firstChild).toMatchSnapshot() + `, + }, + { + // Example from discussions in issue #386 + code: ` + import { render } from '${testingFramework}'; + + function Wrapper({ children }) { + // this should NOT be reported + if (children) { + // ... + } + + // this should NOT be reported + return
{children}
+ }; + + render(); + expect(screen.getByText('SomeComponent')).toBeInTheDocument(); + `, + }, + { + code: ` + import userEvent from '@testing-library/user-event'; + import { screen } from '${testingFramework}'; + + const buttonText = screen.getByText('submit'); + const user = userEvent.setup(); + user.click(buttonText); + `, + }, + { + code: ` + import userEvent from '@testing-library/user-event'; + import { screen } from '${testingFramework}'; + + const buttonText = screen.getByText('submit'); + const userAlias = userEvent.setup(); + userAlias.click(buttonText); + `, + }, + { + code: ` + import userEvent from '@testing-library/user-event'; + import { screen } from '${testingFramework}'; + test('...', () => { + const buttonText = screen.getByText('submit'); + (() => { click: userEvent.click(buttonText); })(); + }); + `, + }, + { + code: ` + import userEvent from '@testing-library/user-event'; + import { screen } from '${testingFramework}'; + + const buttonText = screen.getByText('submit'); + userEvent.setup().click(buttonText); + `, + }, + { + code: ` + import userEvt from '@testing-library/user-event'; + import { screen } from '${testingFramework}'; + + const buttonText = screen.getByText('submit'); + const userAlias = userEvt.setup(); + userAlias.click(buttonText); + `, + }, + { + code: ` + import userEvt from '@testing-library/user-event'; + import { screen } from '${testingFramework}'; + + const buttonText = screen.getByText('submit'); + userEvt.click(buttonText); + `, + }, + { + code: ` + import { screen } from '${testingFramework}'; + import userEvent from '@testing-library/user-event'; + + describe('Testing', () => { + let user; + + beforeEach(() => { + user = userEvent.setup(); + }); + + it('test 1', async () => { + await user.click(screen.getByRole('button')); + }); + }); + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + // case: custom module set but not imported using ${testingFramework} (aggressive reporting limited) + import { screen, userEvent } from 'test-utils'; + + describe('Testing', () => { + let user; + + beforeEach(() => { + user = userEvent.setup(); + }); + + it('test 1', async () => { + await user.click(screen.getByRole('button')); + }); + }); + `, + }, + { + code: ` + import { screen, fireEvent as fe } from '${testingFramework}'; + + const buttonText = screen.getByText('submit'); + fe.click(buttonText); + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + // case: custom module set but not imported using ${testingFramework} (aggressive reporting limited) + import { screen, fireEvent as fe } from 'test-utils'; + + const buttonText = screen.getByText('submit'); + fe.click(buttonText); + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + // case: custom module set but not imported using ${testingFramework} (aggressive reporting limited) + import { screen, fireEvent } from '../test-utils'; + + const buttonText = screen.getByText('submit'); + fireEvent.click(buttonText); + `, + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const ui = { + select: screen.getByRole('combobox', {name: 'Test label'}), + }; + test('...', () => { + const select = ui.select.get(); + expect(select).toHaveClass(selectClasses.select); + }); + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + // case: custom module set but not imported using ${testingFramework} (aggressive reporting limited) + import { screen, render } from 'test-utils'; + import MyComponent from './MyComponent' + + test('...', async () => { + const { user } = render() + await user.click(screen.getByRole("button")) + }); + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + // case: custom module set but not imported using ${testingFramework} (aggressive reporting limited) + import { screen, render } from 'test-utils'; + import MyComponent from './MyComponent' + + test('...', async () => { + const result = render() + await result.user.click(screen.getByRole("button")) + }); + `, + }, + { + settings: { + 'testing-library/utils-module': 'TestUtils', + 'testing-library/custom-renders': ['renderComponent'], + }, + code: ` + // case: custom module set but not imported using ${testingFramework} (aggressive reporting limited) + import { screen, renderComponent } from './TestUtils'; + import MyComponent from './MyComponent' + + test('...', async () => { + const result = renderComponent() + await result.user.click(screen.getByRole("button")) + }); + `, + }, + ] + ), + invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case: importing from custom module for ${testingFramework} (aggressive reporting limited) import 'test-utils'; const closestButton = document.getElementById('submit-btn') expect(closestButton).toBeInTheDocument(); `, - errors: [{ line: 4, column: 38, messageId: 'noNodeAccess' }], - }, - { - code: ` - import { screen } from '@testing-library/react'; - + errors: [{ line: 4, column: 38, messageId: 'noNodeAccess' }], + }, + { + code: ` + import { screen } from '${testingFramework}'; + const button = document.getElementById('submit-btn').closest('button'); `, - errors: [ - { - line: 4, - column: 33, - messageId: 'noNodeAccess', - }, - { - line: 4, - column: 62, - messageId: 'noNodeAccess', - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + line: 4, + column: 33, + messageId: 'noNodeAccess', + }, + { + line: 4, + column: 62, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; document.getElementById('submit-btn'); `, - errors: [ - { - line: 4, - column: 18, - messageId: 'noNodeAccess', - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; - + errors: [ + { + line: 4, + column: 18, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + screen.getByText('submit').closest('button'); `, - errors: [ - { - // error points to `closest` - line: 4, - column: 36, - messageId: 'noNodeAccess', - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; - + errors: [ + { + // error points to `closest` + line: 4, + column: 36, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + expect(screen.getByText('submit').closest('button').textContent).toBe('Submit'); `, - errors: [ - { - line: 4, - column: 43, - messageId: 'noNodeAccess', - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; - + errors: [ + { + line: 4, + column: 43, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; + const { getByText } = render() getByText('submit').closest('button'); `, - errors: [{ line: 5, column: 29, messageId: 'noNodeAccess' }], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [{ line: 5, column: 29, messageId: 'noNodeAccess' }], + }, + { + code: ` + import { screen } from '${testingFramework}'; const buttons = screen.getAllByRole('button'); const childA = buttons[1].firstChild; const button = buttons[2]; button.lastChild `, - errors: [ - { - // error points to `firstChild` - line: 5, - column: 35, - messageId: 'noNodeAccess', - }, - { - // error points to `lastChild` - line: 7, - column: 16, - messageId: 'noNodeAccess', - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; - + errors: [ + { + // error points to `firstChild` + line: 5, + column: 35, + messageId: 'noNodeAccess', + }, + { + // error points to `lastChild` + line: 7, + column: 16, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + const buttonText = screen.getByText('submit'); const button = buttonText.closest('button'); `, - errors: [{ line: 5, column: 35, messageId: 'noNodeAccess' }], - }, - { - code: ` - import { render } from '@testing-library/react'; - + errors: [{ line: 5, column: 35, messageId: 'noNodeAccess' }], + }, + { + code: ` + import { render } from '${testingFramework}'; + const { getByText } = render() const buttonText = getByText('submit'); const button = buttonText.closest('button'); `, - errors: [ - { - line: 6, - column: 35, - messageId: 'noNodeAccess', - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; - + errors: [ + { + line: 6, + column: 35, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; + const { getByText } = render() const button = getByText('submit').closest('button'); `, - errors: [{ line: 5, column: 44, messageId: 'noNodeAccess' }], - }, - { - code: ` - import { screen } from '@testing-library/react'; - + errors: [{ line: 5, column: 44, messageId: 'noNodeAccess' }], + }, + { + code: ` + import { screen } from '${testingFramework}'; + function getExampleDOM() { const container = document.createElement('div'); container.innerHTML = \` @@ -245,18 +506,18 @@ ruleTester.run(RULE_NAME, rule, { const buttons = screen.getAllByRole(exampleDOM, 'button'); const buttonText = buttons[1].firstChild; `, - errors: [ - { - // error points to `firstChild` - line: 19, - column: 39, - messageId: 'noNodeAccess', - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + // error points to `firstChild` + line: 19, + column: 39, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; function getExampleDOM() { const container = document.createElement('div'); @@ -275,14 +536,90 @@ ruleTester.run(RULE_NAME, rule, { const submitButton = screen.getByText(exampleDOM, 'Submit'); const previousButton = submitButton.previousSibling; `, - errors: [ - { - // error points to `previousSibling` - line: 19, - column: 45, - messageId: 'noNodeAccess', - }, - ], - }, - ], + errors: [ + { + // error points to `previousSibling` + line: 19, + column: 45, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; + + const { container } = render() + + expect(container.firstChild).toMatchSnapshot() + `, + errors: [ + { + // error points to `firstChild` + line: 6, + column: 26, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; + + const { container } = render(
  • item
  • item
) + + expect(container.childElementCount).toBe(2) + `, + errors: [ + { + // error points to `childElementCount` + line: 6, + column: 26, + messageId: 'noNodeAccess', + }, + ], + }, + ...EVENT_HANDLER_METHODS.flatMap((method) => [ + { + code: ` + import { screen } from '${testingFramework}'; + + const button = document.getElementById('submit-btn').${method}(); + `, + errors: [ + { + line: 4, + column: 33, + messageId: 'noNodeAccess', + }, + { + line: 4, + column: 62, + messageId: 'noNodeAccess', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + // case: custom module set but not imported using ${testingFramework} (aggressive reporting limited) + import { screen } from 'test-utils'; + + const button = document.getElementById('submit-btn').${method}(); + `, + errors: [ + { + line: 5, + column: 33, + messageId: 'noNodeAccess', + }, + { + line: 5, + column: 62, + messageId: 'noNodeAccess', + }, + ], + }, + ]), + ]), }); diff --git a/tests/lib/rules/no-promise-in-fire-event.test.ts b/tests/lib/rules/no-promise-in-fire-event.test.ts index 05425dbf..82800e3f 100644 --- a/tests/lib/rules/no-promise-in-fire-event.test.ts +++ b/tests/lib/rules/no-promise-in-fire-event.test.ts @@ -3,173 +3,188 @@ import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); -ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: ` - import {fireEvent} from '@testing-library/foo'; - - fireEvent.click(screen.getByRole('button')) - `, - }, - { - code: ` - import {fireEvent} from '@testing-library/foo'; - - fireEvent.click(queryByRole('button'))`, - }, - { - code: ` - import {fireEvent} from '@testing-library/foo'; - - fireEvent.click(someRef)`, - }, - { - code: ` - import {fireEvent} from '@testing-library/foo'; - - fireEvent.click(await screen.findByRole('button')) - `, - }, - { - code: ` - import {fireEvent} from '@testing-library/foo' +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; - const elementPromise = screen.findByRole('button') - const button = await elementPromise - fireEvent.click(button)`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `// invalid usage but aggressive reporting opted-out +ruleTester.run(RULE_NAME, rule, { + valid: [ + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import {fireEvent} from '${testingFramework}'; + + fireEvent.click(screen.getByRole('button')) + `, + }, + { + code: ` + import {fireEvent} from '${testingFramework}'; + + fireEvent.click(queryByRole('button')) + `, + }, + { + code: ` + import {fireEvent} from '${testingFramework}'; + + fireEvent.click(someRef) + `, + }, + { + code: ` + import {fireEvent} from '${testingFramework}'; + + fireEvent.click(await screen.findByRole('button')) + `, + }, + { + code: ` + import {fireEvent} from '${testingFramework}' + + const elementPromise = screen.findByRole('button') + const button = await elementPromise + fireEvent.click(button) + `, + }, + ]), + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `// invalid usage but aggressive reporting opted-out import { fireEvent } from 'somewhere-else' fireEvent.click(findByText('submit')) `, - }, - `// edge case for coverage: + }, + `// edge case for coverage: // valid use case without call expression // so there is no innermost function scope found test('edge case for no innermost function scope', () => { const click = fireEvent.click }) `, - `// edge case for coverage: + `// edge case for coverage: // new expression of something else than Promise fireEvent.click(new SomeElement()) `, - ], - invalid: [ - { - // aggressive reporting opted-in - code: `fireEvent.click(findByText('submit'))`, - errors: [ - { - messageId: 'noPromiseInFireEvent', - line: 1, - column: 17, - endColumn: 37, - }, - ], - }, - { - // aggressive reporting opted-in - code: `fireEvent.click(Promise())`, - errors: [ - { - messageId: 'noPromiseInFireEvent', - line: 1, - column: 17, - endColumn: 26, - }, - ], - }, - { - code: ` - import {fireEvent} from '@testing-library/foo'; + ], + invalid: [ + { + // aggressive reporting opted-in + code: `fireEvent.click(findByText('submit'))`, + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 1, + column: 17, + endColumn: 37, + }, + ], + }, + { + // aggressive reporting opted-in + code: `fireEvent.click(Promise())`, + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 1, + column: 17, + endColumn: 26, + }, + ], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import {fireEvent} from '${testingFramework}'; const promise = new Promise(); fireEvent.click(promise)`, - errors: [ - { - messageId: 'noPromiseInFireEvent', - line: 5, - column: 25, - endColumn: 32, - }, - ], - }, - { - code: ` - import {fireEvent} from '@testing-library/foo' + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 5, + column: 25, + endColumn: 32, + }, + ], + } as const, + { + code: ` + import {fireEvent} from '${testingFramework}' const elementPromise = screen.findByRole('button') fireEvent.click(elementPromise)`, - errors: [ - { - messageId: 'noPromiseInFireEvent', - line: 5, - column: 25, - endColumn: 39, - }, - ], - }, - { - code: ` - import {fireEvent} from '@testing-library/foo'; + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 5, + column: 25, + endColumn: 39, + }, + ], + } as const, + { + code: ` + import {fireEvent} from '${testingFramework}'; fireEvent.click(screen.findByRole('button'))`, - errors: [ - { - messageId: 'noPromiseInFireEvent', - line: 4, - column: 25, - endColumn: 52, - }, - ], - }, - { - code: ` - import {fireEvent} from '@testing-library/foo'; + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 4, + column: 25, + endColumn: 52, + }, + ], + } as const, + { + code: ` + import {fireEvent} from '${testingFramework}'; fireEvent.click(findByText('submit'))`, - errors: [ - { - messageId: 'noPromiseInFireEvent', - line: 4, - column: 25, - endColumn: 45, - }, - ], - }, - { - code: ` - import {fireEvent} from '@testing-library/foo'; + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 4, + column: 25, + endColumn: 45, + }, + ], + } as const, + { + code: ` + import {fireEvent} from '${testingFramework}'; fireEvent.click(Promise('foo'))`, - errors: [ - { - messageId: 'noPromiseInFireEvent', - line: 4, - column: 25, - endColumn: 39, - }, - ], - }, - { - code: ` - import {fireEvent} from '@testing-library/foo'; + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 4, + column: 25, + endColumn: 39, + }, + ], + } as const, + { + code: ` + import {fireEvent} from '${testingFramework}'; fireEvent.click(new Promise('foo'))`, - errors: [ - { - messageId: 'noPromiseInFireEvent', - line: 4, - column: 25, - endColumn: 43, - }, - ], - }, - ], + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 4, + column: 25, + endColumn: 43, + }, + ], + } as const, + ]), + ], }); diff --git a/tests/lib/rules/no-render-in-lifecycle.test.ts b/tests/lib/rules/no-render-in-lifecycle.test.ts new file mode 100644 index 00000000..1561b394 --- /dev/null +++ b/tests/lib/rules/no-render-in-lifecycle.test.ts @@ -0,0 +1,280 @@ +import rule, { RULE_NAME } from '../../../lib/rules/no-render-in-lifecycle'; +import { TESTING_FRAMEWORK_SETUP_HOOKS } from '../../../lib/utils'; +import { createRuleTester } from '../test-utils'; + +const ruleTester = createRuleTester(); + +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + +ruleTester.run(RULE_NAME, rule, { + valid: [ + ...SUPPORTED_TESTING_FRAMEWORKS.map((testingFramework) => ({ + code: ` + import { render } from '${testingFramework}'; + + beforeAll(() => { + doOtherStuff(); + }); + + beforeEach(() => { + doSomethingElse(); + }); + + it('Test', () => { + render() + }) + `, + })), + // test config options + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { render } from '${testingFramework}'; + beforeAll(() => { + render(); + }); + `, + options: [{ allowTestingFrameworkSetupHook: 'beforeAll' }], + }, + { + code: ` + import { render } from '${testingFramework}'; + beforeEach(() => { + render(); + }); + `, + options: [{ allowTestingFrameworkSetupHook: 'beforeEach' }], + }, + ]), + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from 'imNoTestingLibrary'; + ${setupHook}(() => { + render() + }) + `, + })), + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((allowedSetupHook) => { + const [disallowedHook] = TESTING_FRAMEWORK_SETUP_HOOKS.filter( + (setupHook) => setupHook !== allowedSetupHook + ); + return { + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['show', 'renderWithRedux'], + }, + code: ` + import utils from 'imNoTestingLibrary'; + import { show } from '../test-utils'; + ${allowedSetupHook}(() => { + show() + }) + ${disallowedHook}(() => { + utils.render() + }) + `, + options: [ + { + allowTestingFrameworkSetupHook: allowedSetupHook, + }, + ], + }; + }), + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + const { render } = require('imNoTestingLibrary') + + ${setupHook}(() => { + render() + }) + `, + errors: [ + { + messageId: 'noRenderInSetup', + }, + ], + })), + ], + + invalid: [ + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + code: ` + import { render } from '${testingFramework}'; + ${setupHook}(() => { + render() + }) + `, + errors: [ + { + line: 4, + column: 11, + messageId: 'noRenderInSetup', + }, + ], + }) as const + ), + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + code: ` + import { render } from '${testingFramework}'; + ${setupHook}(function() { + render() + }) + `, + errors: [ + { + line: 4, + column: 11, + messageId: 'noRenderInSetup', + }, + ], + }) as const + ), + ]), + // custom render function + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['show', 'renderWithRedux'], + }, + code: ` + import { show } from '../test-utils'; + + ${setupHook}(() => { + show() + }) + `, + errors: [ + { + line: 5, + column: 11, + messageId: 'noRenderInSetup', + }, + ], + }) as const + ), + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + code: `// call render within a wrapper function + import { render } from '${testingFramework}'; + + const wrapper = () => render() + + ${setupHook}(() => { + wrapper() + }) + `, + errors: [ + { + line: 7, + column: 9, + messageId: 'noRenderInSetup', + }, + ], + }) as const + ), + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((allowedSetupHook) => { + const [disallowedHook] = TESTING_FRAMEWORK_SETUP_HOOKS.filter( + (setupHook) => setupHook !== allowedSetupHook + ); + return { + code: ` + import { render } from '${testingFramework}'; + ${disallowedHook}(() => { + render() + }) + `, + options: [ + { + allowTestingFrameworkSetupHook: allowedSetupHook, + }, + ], + errors: [ + { + line: 4, + column: 13, + messageId: 'noRenderInSetup', + }, + ], + } as const; + }), + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + code: ` + import * as testingLibrary from '${testingFramework}'; + ${setupHook}(() => { + testingLibrary.render() + }) + `, + errors: [ + { + line: 4, + column: 26, + messageId: 'noRenderInSetup', + }, + ], + }) as const + ), + ]), + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from 'imNoTestingLibrary'; + import * as testUtils from '../test-utils'; + ${setupHook}(() => { + testUtils.renderWithRedux() + }) + it('Test', () => { + render() + }) + `, + errors: [ + { + line: 5, + column: 21, + messageId: 'noRenderInSetup', + }, + ], + }) as const + ), + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => + TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + code: ` + const { render } = require('${testingFramework}') + + ${setupHook}(() => { + render() + }) + `, + errors: [ + { + line: 5, + column: 11, + messageId: 'noRenderInSetup', + }, + ], + }) as const + ) + ), + ], +}); diff --git a/tests/lib/rules/no-render-in-setup.test.ts b/tests/lib/rules/no-render-in-setup.test.ts deleted file mode 100644 index 4db0796a..00000000 --- a/tests/lib/rules/no-render-in-setup.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -import rule, { RULE_NAME } from '../../../lib/rules/no-render-in-setup'; -import { TESTING_FRAMEWORK_SETUP_HOOKS } from '../../../lib/utils'; -import { createRuleTester } from '../test-utils'; - -const ruleTester = createRuleTester(); - -ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: ` - import { render } from '@testing-library/foo'; - - beforeAll(() => { - doOtherStuff(); - }); - - beforeEach(() => { - doSomethingElse(); - }); - - it('Test', () => { - render() - }) - `, - }, - // test config options - { - code: ` - import { render } from '@testing-library/foo'; - beforeAll(() => { - render(); - }); - `, - options: [{ allowTestingFrameworkSetupHook: 'beforeAll' }], - }, - { - code: ` - import { render } from '@testing-library/foo'; - beforeEach(() => { - render(); - }); - `, - options: [{ allowTestingFrameworkSetupHook: 'beforeEach' }], - }, - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from 'imNoTestingLibrary'; - ${setupHook}(() => { - render() - }) - `, - })), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((allowedSetupHook) => { - const [disallowedHook] = TESTING_FRAMEWORK_SETUP_HOOKS.filter( - (setupHook) => setupHook !== allowedSetupHook - ); - return { - settings: { - 'testing-library/utils-module': 'test-utils', - 'testing-library/custom-renders': ['show', 'renderWithRedux'], - }, - code: ` - import utils from 'imNoTestingLibrary'; - import { show } from '../test-utils'; - ${allowedSetupHook}(() => { - show() - }) - ${disallowedHook}(() => { - utils.render() - }) - `, - options: [ - { - allowTestingFrameworkSetupHook: allowedSetupHook, - }, - ], - }; - }), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - const { render } = require('imNoTestingLibrary') - - ${setupHook}(() => { - render() - }) - `, - errors: [ - { - messageId: 'noRenderInSetup', - }, - ], - })), - ], - - invalid: [ - ...TESTING_FRAMEWORK_SETUP_HOOKS.map( - (setupHook) => - ({ - code: ` - import { render } from '@testing-library/foo'; - ${setupHook}(() => { - render() - }) - `, - errors: [ - { - line: 4, - column: 11, - messageId: 'noRenderInSetup', - }, - ], - } as const) - ), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map( - (setupHook) => - ({ - code: ` - import { render } from '@testing-library/foo'; - ${setupHook}(function() { - render() - }) - `, - errors: [ - { - line: 4, - column: 11, - messageId: 'noRenderInSetup', - }, - ], - } as const) - ), - // custom render function - ...TESTING_FRAMEWORK_SETUP_HOOKS.map( - (setupHook) => - ({ - settings: { - 'testing-library/utils-module': 'test-utils', - 'testing-library/custom-renders': ['show', 'renderWithRedux'], - }, - code: ` - import { show } from '../test-utils'; - - ${setupHook}(() => { - show() - }) - `, - errors: [ - { - line: 5, - column: 11, - messageId: 'noRenderInSetup', - }, - ], - } as const) - ), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map( - (setupHook) => - ({ - code: `// call render within a wrapper function - import { render } from '@testing-library/foo'; - - const wrapper = () => render() - - ${setupHook}(() => { - wrapper() - }) - `, - errors: [ - { - line: 7, - column: 9, - messageId: 'noRenderInSetup', - }, - ], - } as const) - ), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((allowedSetupHook) => { - const [disallowedHook] = TESTING_FRAMEWORK_SETUP_HOOKS.filter( - (setupHook) => setupHook !== allowedSetupHook - ); - return { - code: ` - import { render } from '@testing-library/foo'; - ${disallowedHook}(() => { - render() - }) - `, - options: [ - { - allowTestingFrameworkSetupHook: allowedSetupHook, - }, - ], - errors: [ - { - line: 4, - column: 13, - messageId: 'noRenderInSetup', - }, - ], - } as const; - }), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map( - (setupHook) => - ({ - code: ` - import * as testingLibrary from '@testing-library/foo'; - ${setupHook}(() => { - testingLibrary.render() - }) - `, - errors: [ - { - line: 4, - column: 26, - messageId: 'noRenderInSetup', - }, - ], - } as const) - ), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map( - (setupHook) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from 'imNoTestingLibrary'; - import * as testUtils from '../test-utils'; - ${setupHook}(() => { - testUtils.renderWithRedux() - }) - it('Test', () => { - render() - }) - `, - errors: [ - { - line: 5, - column: 21, - messageId: 'noRenderInSetup', - }, - ], - } as const) - ), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map( - (setupHook) => - ({ - code: ` - const { render } = require('@testing-library/foo') - - ${setupHook}(() => { - render() - }) - `, - errors: [ - { - line: 5, - column: 11, - messageId: 'noRenderInSetup', - }, - ], - } as const) - ), - ], -}); diff --git a/tests/lib/rules/no-test-id-queries.test.ts b/tests/lib/rules/no-test-id-queries.test.ts new file mode 100644 index 00000000..23ba3335 --- /dev/null +++ b/tests/lib/rules/no-test-id-queries.test.ts @@ -0,0 +1,86 @@ +import rule, { RULE_NAME } from '../../../lib/rules/no-test-id-queries'; +import { createRuleTester } from '../test-utils'; + +const ruleTester = createRuleTester(); + +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + +const QUERIES = [ + 'getByTestId', + 'queryByTestId', + 'getAllByTestId', + 'queryAllByTestId', + 'findByTestId', + 'findAllByTestId', +]; + +ruleTester.run(RULE_NAME, rule, { + valid: [ + ` + import { render } from '@testing-library/react'; + + test('test', async () => { + const { getByRole } = render(); + + expect(getByRole('button')).toBeInTheDocument(); + }); + `, + + ` + import { render } from '@testing-library/react'; + + test('test', async () => { + render(); + + expect(getTestId('button')).toBeInTheDocument(); + }); + `, + ], + + invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((framework) => + QUERIES.flatMap((query) => [ + { + code: ` + import { render } from '${framework}'; + + test('test', async () => { + const { ${query} } = render(); + + expect(${query}('my-test-id')).toBeInTheDocument(); + }); + `, + errors: [ + { + messageId: 'noTestIdQueries', + line: 7, + column: 14, + }, + ], + }, + { + code: ` + import { render, screen } from '${framework}'; + + test('test', async () => { + render(); + + expect(screen.${query}('my-test-id')).toBeInTheDocument(); + }); + `, + errors: [ + { + messageId: 'noTestIdQueries', + line: 7, + column: 14, + }, + ], + }, + ]) + ), +}); diff --git a/tests/lib/rules/no-unnecessary-act.test.ts b/tests/lib/rules/no-unnecessary-act.test.ts index 62407e30..26305961 100644 --- a/tests/lib/rules/no-unnecessary-act.test.ts +++ b/tests/lib/rules/no-unnecessary-act.test.ts @@ -1,40 +1,48 @@ -import type { TSESLint } from '@typescript-eslint/experimental-utils'; +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; import rule, { - MessageIds, - Options, - RULE_NAME, + MessageIds, + Options, + RULE_NAME, } from '../../../lib/rules/no-unnecessary-act'; import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); -type ValidTestCase = TSESLint.ValidTestCase; -type InvalidTestCase = TSESLint.InvalidTestCase; -type TestCase = InvalidTestCase | ValidTestCase; +type RuleValidTestCase = ValidTestCase; +type RuleInvalidTestCase = InvalidTestCase; +type TestCase = RuleInvalidTestCase | RuleValidTestCase; const addOptions = ( - array: T[], - options?: Options[number] + array: T[], + options?: Options[number] ): T[] => - array.map((testCase) => ({ - ...testCase, - options: [options], - })); + array.map((testCase) => ({ + ...testCase, + options: [options], + })); const disableStrict = (array: T[]): T[] => - addOptions(array, { isStrict: false }); + addOptions(array, { isStrict: false }); const enableStrict = (array: T[]): T[] => - addOptions(array, { isStrict: true }); + addOptions(array, { isStrict: true }); /** * - AGR stands for Aggressive Reporting * - RTL stands for React Testing Library (@testing-library/react) + * - Marko TL stands for Marko Testing Library (@marko/testing-library) * - RTU stands for React Test Utils (react-dom/test-utils) */ +const SUPPORTED_TESTING_FRAMEWORKS = [ + ['@testing-library/react', 'RTL'], + ['@marko/testing-library', 'Marko TL'], +]; -const validNonStrictTestCases: ValidTestCase[] = [ - { - code: `// case: RTL act wrapping both RTL and non-RTL calls +const validNonStrictTestCases: RuleValidTestCase[] = [ + { + code: `// case: RTL act wrapping both RTL and non-RTL calls import { act, render, waitFor } from '@testing-library/react' test('valid case', async () => { @@ -54,13 +62,13 @@ const validNonStrictTestCases: ValidTestCase[] = [ }); }); `, - }, + }, ]; -const validTestCases: ValidTestCase[] = [ - { - code: `// case: RTL act wrapping non-RTL calls - import { act } from '@testing-library/react' +const validTestCases: RuleValidTestCase[] = [ + ...SUPPORTED_TESTING_FRAMEWORKS.map(([testingFramework, shortName]) => ({ + code: `// case: ${shortName} act wrapping non-${shortName} calls + import { act } from '${testingFramework}' test('valid case', async () => { act(() => { @@ -118,9 +126,9 @@ const validTestCases: ValidTestCase[] = [ act(stuffThatDoesNotUseRTL().then(() => {})) }); `, - }, - { - code: `// case: RTU act wrapping non-RTL + })), + { + code: `// case: RTU act wrapping non-RTL import { act } from 'react-dom/test-utils' test('valid case', async () => { @@ -143,13 +151,13 @@ const validTestCases: ValidTestCase[] = [ act(() => stuffThatDoesNotUseRTL()); }); `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `// case: RTL act wrapping non-RTL - AGR disabled - import { act } from '@testing-library/react' + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map(([testingFramework, shortName]) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `// case: ${shortName} act wrapping non-${shortName} - AGR disabled + import { act } from '${testingFramework}' import { waitFor } from 'somewhere-else' test('valid case', async () => { @@ -172,15 +180,15 @@ const validTestCases: ValidTestCase[] = [ act(() => waitFor()); }); `, - }, + })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `// case: non-RTL act wrapping RTL - AGR disabled + ...SUPPORTED_TESTING_FRAMEWORKS.map(([testingFramework, shortName]) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `// case: non-${shortName} act wrapping ${shortName} - AGR disabled import { act } from 'somewhere-else' - import { waitFor } from '@testing-library/react' + import { waitFor } from '${testingFramework}' test('valid case', async () => { act(() => { @@ -206,13 +214,14 @@ const validTestCases: ValidTestCase[] = [ act(function() {}) }); `, - }, + })), ]; -const invalidStrictTestCases: InvalidTestCase[] = [ - { - code: `// case: RTL act wrapping both RTL and non-RTL calls with strict option - import { act, render } from '@testing-library/react' +const invalidStrictTestCases: RuleInvalidTestCase[] = + SUPPORTED_TESTING_FRAMEWORKS.flatMap(([testingFramework, shortName]) => [ + { + code: `// case: ${shortName} act wrapping both ${shortName} and non-${shortName} calls with strict option + import { act, render } from '${testingFramework}' await act(async () => { userEvent.click(screen.getByText("Submit")) @@ -223,25 +232,27 @@ const invalidStrictTestCases: InvalidTestCase[] = [ flushPromises() }) `, - errors: [ - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 4, - column: 13, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 8, - column: 7, - }, - ], - }, -]; - -const invalidTestCases: InvalidTestCase[] = [ - { - code: `// case: RTL act wrapping RTL calls - callbacks with body (BlockStatement) - import { act, fireEvent, screen, render, waitFor, waitForElementToBeRemoved } from '@testing-library/react' + errors: [ + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 4, + column: 13, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 8, + column: 7, + }, + ], + }, + ]); + +const invalidTestCases: RuleInvalidTestCase[] = [ + ...SUPPORTED_TESTING_FRAMEWORKS.map( + ([testingFramework, shortName]) => + ({ + code: `// case: ${shortName} act wrapping ${shortName} calls - callbacks with body (BlockStatement) + import { act, fireEvent, screen, render, waitFor, waitForElementToBeRemoved } from '${testingFramework}' import userEvent from '@testing-library/user-event' test('invalid case', async () => { @@ -280,50 +291,55 @@ const invalidTestCases: InvalidTestCase[] = [ }); }); `, - errors: [ - { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 6, column: 9 }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 10, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 14, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 18, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 22, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 26, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 30, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 34, - column: 9, - }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `// case: RTL act wrapping RTL calls - callbacks with body (BlockStatement) - AGR disabled + errors: [ + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 6, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 10, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 14, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 18, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 22, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 26, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 30, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 34, + column: 9, + }, + ], + }) as const + ), + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `// case: RTL act wrapping RTL calls - callbacks with body (BlockStatement) - AGR disabled import { act, fireEvent, screen, render, waitFor, waitForElementToBeRemoved } from 'test-utils' import userEvent from '@testing-library/user-event' @@ -363,47 +379,47 @@ const invalidTestCases: InvalidTestCase[] = [ }); }); `, - errors: [ - { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 6, column: 9 }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 10, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 14, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 18, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 22, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 26, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 30, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 34, - column: 9, - }, - ], - }, - { - code: `// case: RTL act wrapping RTL calls - callbacks with return + errors: [ + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 6, column: 9 }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 10, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 14, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 18, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 22, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 26, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 30, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 34, + column: 9, + }, + ], + }, + { + code: `// case: RTL act wrapping RTL calls - callbacks with return import { act, fireEvent, screen, render, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -450,90 +466,90 @@ const invalidTestCases: InvalidTestCase[] = [ }).then(() => {}) }); `, - errors: [ - { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 6, column: 9 }, - { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 7, column: 9 }, - { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 8, column: 9 }, - { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 9, column: 9 }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 10, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 11, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 12, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 13, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 14, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 16, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 19, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 22, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 25, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 28, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 31, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 34, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 37, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 40, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 43, - column: 9, - }, - ], - }, - { - code: `// case: RTL act wrapping empty callback + errors: [ + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 6, column: 9 }, + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 7, column: 9 }, + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 8, column: 9 }, + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 9, column: 9 }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 10, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 11, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 12, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 13, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 14, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 16, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 19, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 22, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 25, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 28, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 31, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 34, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 37, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 40, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 43, + column: 9, + }, + ], + }, + { + code: `// case: RTL act wrapping empty callback import { act } from '@testing-library/react' test('invalid case', async () => { @@ -543,18 +559,18 @@ const invalidTestCases: InvalidTestCase[] = [ act(function () {}) }) `, - errors: [ - { messageId: 'noUnnecessaryActEmptyFunction', line: 5, column: 15 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 6, column: 9 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 7, column: 15 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 9 }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `// case: RTL act wrapping empty callback - require version + errors: [ + { messageId: 'noUnnecessaryActEmptyFunction', line: 5, column: 15 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 6, column: 9 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 7, column: 15 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 9 }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `// case: RTL act wrapping empty callback - require version const { act } = require('@testing-library/react'); test('invalid case', async () => { @@ -565,21 +581,21 @@ const invalidTestCases: InvalidTestCase[] = [ act(function () {}).then(() => {}) }) `, - errors: [ - { messageId: 'noUnnecessaryActEmptyFunction', line: 5, column: 15 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 6, column: 9 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 7, column: 15 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 9 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 9, column: 9 }, - ], - }, - - // cases for act related to React Test Utils - { - settings: { - 'testing-library/utils-module': 'custom-testing-module', - }, - code: `// case: RTU act wrapping RTL calls - callbacks with body (BlockStatement) + errors: [ + { messageId: 'noUnnecessaryActEmptyFunction', line: 5, column: 15 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 6, column: 9 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 7, column: 15 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 9 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 9, column: 9 }, + ], + }, + + // cases for act related to React Test Utils + { + settings: { + 'testing-library/utils-module': 'custom-testing-module', + }, + code: `// case: RTU act wrapping RTL calls - callbacks with body (BlockStatement) import { act } from 'react-dom/test-utils'; import { fireEvent, screen, render, waitFor, waitForElementToBeRemoved } from 'custom-testing-module' import userEvent from '@testing-library/user-event' @@ -620,50 +636,50 @@ const invalidTestCases: InvalidTestCase[] = [ }); }); `, - errors: [ - { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 7, column: 9 }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 11, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 15, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 19, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 23, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 27, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 31, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 35, - column: 9, - }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'custom-testing-module', - }, - code: `// case: RTU act wrapping RTL calls - callbacks with return + errors: [ + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 7, column: 9 }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 11, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 15, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 19, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 23, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 27, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 31, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 35, + column: 9, + }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'custom-testing-module', + }, + code: `// case: RTU act wrapping RTL calls - callbacks with return import { act } from 'react-dom/test-utils'; import { fireEvent, screen, render, waitFor } from 'custom-testing-module' import userEvent from '@testing-library/user-event' @@ -708,92 +724,92 @@ const invalidTestCases: InvalidTestCase[] = [ }); }); `, - errors: [ - { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 7, column: 9 }, - { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 8, column: 9 }, - { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 9, column: 9 }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 10, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 11, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 12, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 13, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 14, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 15, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 17, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 20, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 23, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 26, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 29, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 32, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 35, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 38, - column: 9, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 41, - column: 15, - }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'off', - }, - code: `// case: RTU act wrapping empty callback + errors: [ + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 7, column: 9 }, + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 8, column: 9 }, + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 9, column: 9 }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 10, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 11, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 12, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 13, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 14, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 15, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 17, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 20, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 23, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 26, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 29, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 32, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 35, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 38, + column: 9, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 41, + column: 15, + }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'off', + }, + code: `// case: RTU act wrapping empty callback import { act } from 'react-dom/test-utils'; import { render } from '@testing-library/react' @@ -805,18 +821,18 @@ const invalidTestCases: InvalidTestCase[] = [ act(function () {}); }); `, - errors: [ - { messageId: 'noUnnecessaryActEmptyFunction', line: 7, column: 15 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 9 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 9, column: 15 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 10, column: 9 }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'off', - }, - code: `// case: RTU act wrapping empty callback - require version + errors: [ + { messageId: 'noUnnecessaryActEmptyFunction', line: 7, column: 15 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 9 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 9, column: 15 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 10, column: 9 }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'off', + }, + code: `// case: RTU act wrapping empty callback - require version const { act } = require('react-dom/test-utils'); const { render } = require('@testing-library/react'); @@ -828,20 +844,20 @@ const invalidTestCases: InvalidTestCase[] = [ act(function () {}); }) `, - errors: [ - { messageId: 'noUnnecessaryActEmptyFunction', line: 7, column: 15 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 9 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 9, column: 15 }, - { messageId: 'noUnnecessaryActEmptyFunction', line: 10, column: 9 }, - ], - }, - - { - settings: { - 'testing-library/utils-module': 'custom-testing-module', - 'testing-library/custom-renders': 'off', - }, - code: `// case: mixed scenarios - AGR disabled + errors: [ + { messageId: 'noUnnecessaryActEmptyFunction', line: 7, column: 15 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 9 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 9, column: 15 }, + { messageId: 'noUnnecessaryActEmptyFunction', line: 10, column: 9 }, + ], + }, + + { + settings: { + 'testing-library/utils-module': 'custom-testing-module', + 'testing-library/custom-renders': 'off', + }, + code: `// case: mixed scenarios - AGR disabled import * as ReactTestUtils from 'react-dom/test-utils'; import { act as renamedAct, fireEvent, screen as renamedScreen, render, waitFor } from 'custom-testing-module' import userEvent from '@testing-library/user-event' @@ -860,36 +876,36 @@ const invalidTestCases: InvalidTestCase[] = [ act(function() { return renamedScreen.getByText('foo') }) }); `, - errors: [ - { messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 24 }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 9, - column: 30, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 10, - column: 15, - }, - { - messageId: 'noUnnecessaryActTestingLibraryUtil', - line: 11, - column: 9, - }, - ], - }, + errors: [ + { messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 24 }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 9, + column: 30, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 10, + column: 15, + }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 11, + column: 9, + }, + ], + }, ]; ruleTester.run(RULE_NAME, rule, { - valid: [ - ...validTestCases, - ...disableStrict(validNonStrictTestCases), - ...disableStrict(validTestCases), - ], - invalid: [ - ...invalidTestCases, - ...enableStrict(invalidStrictTestCases), - ...disableStrict(invalidTestCases), - ], + valid: [ + ...validTestCases, + ...disableStrict(validNonStrictTestCases), + ...disableStrict(validTestCases), + ], + invalid: [ + ...invalidTestCases, + ...enableStrict(invalidStrictTestCases), + ...disableStrict(invalidTestCases), + ], }); diff --git a/tests/lib/rules/no-wait-for-empty-callback.test.ts b/tests/lib/rules/no-wait-for-empty-callback.test.ts deleted file mode 100644 index d9dae719..00000000 --- a/tests/lib/rules/no-wait-for-empty-callback.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import rule, { RULE_NAME } from '../../../lib/rules/no-wait-for-empty-callback'; -import { createRuleTester } from '../test-utils'; - -const ruleTester = createRuleTester(); - -const ALL_WAIT_METHODS = ['waitFor', 'waitForElementToBeRemoved']; - -ruleTester.run(RULE_NAME, rule, { - valid: [ - ...ALL_WAIT_METHODS.map((m) => ({ - code: `${m}(() => { - screen.getByText(/submit/i) - })`, - })), - ...ALL_WAIT_METHODS.map((m) => ({ - code: `${m}(function() { - screen.getByText(/submit/i) - })`, - })), - { - code: `waitForElementToBeRemoved(someNode)`, - }, - { - code: `waitForElementToBeRemoved(() => someNode)`, - }, - { - code: `waitSomethingElse(() => {})`, - }, - { - code: `wait(() => {})`, - }, - { - code: `wait(noop)`, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { waitFor } from 'somewhere-else' - waitFor(() => {}) - `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { waitFor as renamedWaitFor } from '@testing-library/react' - import { waitFor } from 'somewhere-else' - waitFor(() => {}) - `, - }, - ], - - invalid: [ - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(() => {})`, - errors: [ - { - line: 1, - column: 8 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { ${m} } from 'test-utils'; - ${m}(() => {}); - `, - errors: [ - { - line: 3, - column: 16 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { ${m} as renamedAsyncUtil } from 'test-utils'; - renamedAsyncUtil(() => {}); - `, - errors: [ - { - line: 3, - column: 32, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: 'renamedAsyncUtil', - }, - }, - ], - } as const) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}((a, b) => {})`, - errors: [ - { - line: 1, - column: 12 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(() => { /* I'm empty anyway */ })`, - errors: [ - { - line: 1, - column: 8 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(function() { - - })`, - errors: [ - { - line: 1, - column: 13 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(function(a) { - - })`, - errors: [ - { - line: 1, - column: 14 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(function() { - // another empty callback - })`, - errors: [ - { - line: 1, - column: 13 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(noop)`, - errors: [ - { - line: 1, - column: 2 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ], -}); diff --git a/tests/lib/rules/no-wait-for-multiple-assertions.test.ts b/tests/lib/rules/no-wait-for-multiple-assertions.test.ts index 84a3f4be..46f30b7e 100644 --- a/tests/lib/rules/no-wait-for-multiple-assertions.test.ts +++ b/tests/lib/rules/no-wait-for-multiple-assertions.test.ts @@ -1,148 +1,159 @@ import rule, { - RULE_NAME, + RULE_NAME, } from '../../../lib/rules/no-wait-for-multiple-assertions'; import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: ` + valid: [ + { + code: ` await waitFor(() => expect(a).toEqual('a')) `, - }, - { - code: ` + }, + { + code: ` await waitFor(function() { expect(a).toEqual('a') }) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// Aggressive Reporting disabled - module imported not matching + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// Aggressive Reporting disabled - module imported not matching import { waitFor } from 'somewhere-else' await waitFor(() => { expect(a).toEqual('a') expect(b).toEqual('b') }) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// Aggressive Reporting disabled - waitFor renamed - import { waitFor as renamedWaitFor } from '@testing-library/react' + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map((testingFramework) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// Aggressive Reporting disabled - waitFor renamed + import { waitFor as renamedWaitFor } from '${testingFramework}' import { waitFor } from 'somewhere-else' await waitFor(() => { expect(a).toEqual('a') expect(b).toEqual('b') }) `, - }, - // this needs to be check by other rule - { - code: ` + })), + // this needs to be check by other rule + { + code: ` await waitFor(() => { fireEvent.keyDown(input, {key: 'ArrowDown'}) expect(b).toEqual('b') }) `, - }, - { - code: ` + }, + { + code: ` await waitFor(function() { fireEvent.keyDown(input, {key: 'ArrowDown'}) expect(b).toEqual('b') }) `, - }, - { - code: ` + }, + { + code: ` await waitFor(() => { console.log('testing-library') expect(b).toEqual('b') }) `, - }, - { - code: ` + }, + { + code: ` await waitFor(function() { console.log('testing-library') expect(b).toEqual('b') }) `, - }, - { - code: ` + }, + { + code: ` await waitFor(() => {}) `, - }, - { - code: ` + }, + { + code: ` await waitFor(function() {}) `, - }, - { - code: ` + }, + { + code: ` await waitFor(() => { // testing }) `, - }, - ], - invalid: [ - { - code: ` + }, + ], + invalid: [ + { + code: ` await waitFor(() => { expect(a).toEqual('a') expect(b).toEqual('b') }) `, - errors: [ - { line: 4, column: 11, messageId: 'noWaitForMultipleAssertion' }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// Aggressive Reporting disabled - import { waitFor } from '@testing-library/react' + errors: [ + { line: 4, column: 11, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// Aggressive Reporting disabled + import { waitFor } from '${testingFramework}' await waitFor(() => { expect(a).toEqual('a') expect(b).toEqual('b') }) `, - errors: [ - { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// Aggressive Reporting disabled + errors: [ + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, + ], + }) as const + ), + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// Aggressive Reporting disabled import { waitFor as renamedWaitFor } from 'test-utils' await renamedWaitFor(() => { expect(a).toEqual('a') expect(b).toEqual('b') }) `, - errors: [ - { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, - ], - }, - { - code: ` + errors: [ + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` await waitFor(() => { expect(a).toEqual('a') console.log('testing-library') expect(b).toEqual('b') }) `, - errors: [ - { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, - ], - }, - { - code: ` + errors: [ + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` test('should whatever', async () => { await waitFor(() => { expect(a).toEqual('a') @@ -151,24 +162,24 @@ ruleTester.run(RULE_NAME, rule, { }) }) `, - errors: [ - { line: 6, column: 13, messageId: 'noWaitForMultipleAssertion' }, - ], - }, - { - code: ` + errors: [ + { line: 6, column: 13, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` await waitFor(async () => { expect(a).toEqual('a') await somethingAsync() expect(b).toEqual('b') }) `, - errors: [ - { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, - ], - }, - { - code: ` + errors: [ + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` await waitFor(function() { expect(a).toEqual('a') expect(b).toEqual('b') @@ -176,35 +187,35 @@ ruleTester.run(RULE_NAME, rule, { expect(d).toEqual('d') }) `, - errors: [ - { line: 4, column: 11, messageId: 'noWaitForMultipleAssertion' }, - { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, - { line: 6, column: 11, messageId: 'noWaitForMultipleAssertion' }, - ], - }, - { - code: ` + errors: [ + { line: 4, column: 11, messageId: 'noWaitForMultipleAssertion' }, + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, + { line: 6, column: 11, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` await waitFor(function() { expect(a).toEqual('a') console.log('testing-library') expect(b).toEqual('b') }) `, - errors: [ - { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, - ], - }, - { - code: ` + errors: [ + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` await waitFor(async function() { expect(a).toEqual('a') const el = await somethingAsync() expect(b).toEqual('b') }) `, - errors: [ - { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, - ], - }, - ], + errors: [ + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + ], }); diff --git a/tests/lib/rules/no-wait-for-side-effects.test.ts b/tests/lib/rules/no-wait-for-side-effects.test.ts index e5102126..c7eed01a 100644 --- a/tests/lib/rules/no-wait-for-side-effects.test.ts +++ b/tests/lib/rules/no-wait-for-side-effects.test.ts @@ -1,135 +1,173 @@ -import rule, { RULE_NAME } from '../../../lib/rules/no-wait-for-side-effects'; +import { InvalidTestCase } from '@typescript-eslint/rule-tester'; + +import rule, { + RULE_NAME, + type MessageIds, +} from '../../../lib/rules/no-wait-for-side-effects'; import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: ` - import { waitFor } from '@testing-library/react'; - await waitFor(() => expect(a).toEqual('a')) - `, - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - await waitFor(function() { - expect(a).toEqual('a') - }) - `, - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - await waitFor(() => { - console.log('testing-library') - expect(b).toEqual('b') - }) - `, - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - await waitFor(function() { - console.log('testing-library') - expect(b).toEqual('b') - }) - `, - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - await waitFor(() => {}) - `, - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - await waitFor(function() {}) - `, - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - await waitFor(() => { - // testing - }) - `, - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - await waitFor(function() { - // testing - }) - `, - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - fireEvent.keyDown(input, {key: 'ArrowDown'}) - await waitFor(() => { - expect(b).toEqual('b') - }) - `, - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - fireEvent.keyDown(input, {key: 'ArrowDown'}) - await waitFor(function() { - expect(b).toEqual('b') - }) - `, - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - userEvent.click(button) - await waitFor(function() { - expect(b).toEqual('b') - }) - `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { waitFor } from 'somewhere-else'; + valid: [ + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { waitFor } from '${testingFramework}'; + await waitFor(() => expect(a).toEqual('a')) + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + await waitFor(function() { + expect(a).toEqual('a') + }) + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + await waitFor(() => { + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + await waitFor(function() { + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + await waitFor(() => {}) + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + await waitFor(function() {}) + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + await waitFor(() => { + // testing + }) + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + await waitFor(function() { + // testing + }) + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + fireEvent.keyDown(input, {key: 'ArrowDown'}) + await waitFor(() => { + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + fireEvent.keyDown(input, {key: 'ArrowDown'}) + await waitFor(function() { + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + userEvent.click(button) + await waitFor(function() { + expect(b).toEqual('b') + }) + `, + }, + { + // Issue #500, https://github.com/testing-library/eslint-plugin-testing-library/issues/500 + code: ` + import { waitFor } from '${testingFramework}'; + userEvent.click(button) + waitFor(function() { + expect(b).toEqual('b') + }).then(() => { + // Side effects are allowed inside .then() + userEvent.click(button); + }) + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + import { notUserEvent } from 'somewhere-else'; + + waitFor(() => { + await notUserEvent.click(button) + }) + `, + }, + ]), + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { waitFor } from 'somewhere-else'; await waitFor(function() { fireEvent.keyDown(input, {key: 'ArrowDown'}) expect(b).toEqual('b') }) `, - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map((testingFramework) => ({ + code: ` + import { waitFor } from '${testingFramework}'; + anotherFunction(() => { fireEvent.keyDown(input, {key: 'ArrowDown'}); userEvent.click(button); }); - + test('side effects in functions other than waitFor are valid', () => { fireEvent.keyDown(input, {key: 'ArrowDown'}) userEvent.click(button) expect(b).toEqual('b') }); `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + })), + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor } from 'somewhere-else'; await waitFor(() => { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor as renamedWaitFor, fireEvent } from 'test-utils'; import { waitFor, userEvent } from 'somewhere-else'; @@ -138,10 +176,10 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor, fireEvent as renamedFireEvent, userEvent as renamedUserEvent } from 'test-utils'; import { fireEvent, userEvent } from 'somewhere-else'; @@ -150,558 +188,594 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - }, - { - code: `// weird case to cover 100% coverage + }, + { + code: `// weird case to cover 100% coverage await waitFor(() => { const click = firEvent['click'] }) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor } from 'somewhere-else'; await waitFor(() => fireEvent.keyDown(input, {key: 'ArrowDown'})) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + ...SUPPORTED_TESTING_FRAMEWORKS.map((testingFramework) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor } from 'somewhere-else'; - import { userEvent } from '@testing-library/react'; + import { userEvent } from '${testingFramework}'; await waitFor(() => userEvent.click(button)) `, - }, - { - settings: { 'testing-library/utils-module': '~/test-utils' }, - code: ` + })), + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` import { waitFor, userEvent } from '~/test-utils'; await waitFor(() => userEvent.click(button)) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor } from 'somewhere-else'; await waitFor(() => render()) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor } from 'somewhere-else'; await waitFor(() => { const { container } = render() }) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor } from 'somewhere-else'; const { rerender } = render() await waitFor(() => { rerender() }) `, - }, - { - settings: { 'testing-library/utils-module': '~/test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` import { waitFor } from '~/test-utils'; import { render } from 'somewhere-else'; await waitFor(() => render()) `, - }, - { - settings: { 'testing-library/utils-module': '~/test-utils' }, - code: ` - import { waitFor } from '@testing-library/react'; - import { render } from 'somewhere-else'; - await waitFor(() => render()) - `, - }, - { - settings: { 'testing-library/custom-renders': ['renderHelper'] }, - code: ` - import { waitFor } from '@testing-library/react'; - import { renderWrapper } from 'somewhere-else'; - await waitFor(() => renderWrapper()) - `, - }, - { - settings: { 'testing-library/custom-renders': ['renderHelper'] }, - code: ` - import { waitFor } from '@testing-library/react'; - import { renderWrapper } from 'somewhere-else'; - await waitFor(() => { - renderWrapper() - }) - `, - }, - { - settings: { 'testing-library/custom-renders': ['renderHelper'] }, - code: ` - import { waitFor } from '@testing-library/react'; - import { renderWrapper } from 'somewhere-else'; - await waitFor(() => { - const { container } = renderWrapper() - }) - `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` + import { waitFor } from '${testingFramework}'; + import { render } from 'somewhere-else'; + await waitFor(() => render()) + `, + }, + { + settings: { 'testing-library/custom-renders': ['renderHelper'] }, + code: ` + import { waitFor } from '${testingFramework}'; + import { renderWrapper } from 'somewhere-else'; + await waitFor(() => renderWrapper()) + `, + }, + { + settings: { 'testing-library/custom-renders': ['renderHelper'] }, + code: ` + import { waitFor } from '${testingFramework}'; + import { renderWrapper } from 'somewhere-else'; + await waitFor(() => { + renderWrapper() + }) + `, + }, + { + settings: { 'testing-library/custom-renders': ['renderHelper'] }, + code: ` + import { waitFor } from '${testingFramework}'; + import { renderWrapper } from 'somewhere-else'; + await waitFor(() => { + const { container } = renderWrapper() + }) + `, + }, + ]), + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor } from 'somewhere-else'; await waitFor(() => { render() }) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor } from 'test-utils'; import { render } from 'somewhere-else'; await waitFor(() => { render() }) `, - }, - { - settings: { 'testing-library/custom-renders': ['renderHelper'] }, - code: ` - import { waitFor } from '@testing-library/react'; - import { renderWrapper } from 'somewhere-else'; - await waitFor(() => { - renderWrapper() - }) - `, - }, - { - settings: { 'testing-library/custom-renders': ['renderHelper'] }, - code: ` - import { waitFor } from '@testing-library/react'; - await waitFor(() => result = renderWrapper()) - `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + settings: { 'testing-library/custom-renders': ['renderHelper'] }, + code: ` + import { waitFor } from '${testingFramework}'; + await waitFor(() => result = renderWrapper()) + `, + }, + ]), + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor } from 'test-utils'; import { render } from 'somewhere-else'; await waitFor(() => result = render()) `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { waitFor } from 'somewhere-else'; await waitFor(() => result = render()) `, - }, - ], - invalid: [ - // render - { - code: ` - import { waitFor } from '@testing-library/react'; + }, + ], + invalid: [ + // render + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => render()) `, - errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(function() { render() }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(function() { const { container } = renderHelper() }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - settings: { 'testing-library/custom-renders': ['renderHelper'] }, - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + settings: { 'testing-library/custom-renders': ['renderHelper'] }, + code: ` + import { waitFor } from '${testingFramework}'; import { renderHelper } from 'somewhere-else'; await waitFor(() => renderHelper()) `, - errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - settings: { 'testing-library/custom-renders': ['renderHelper'] }, - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + settings: { 'testing-library/custom-renders': ['renderHelper'] }, + code: ` + import { waitFor } from '${testingFramework}'; import { renderHelper } from 'somewhere-else'; await waitFor(() => { renderHelper() }) `, - errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - settings: { 'testing-library/custom-renders': ['renderHelper'] }, - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + settings: { 'testing-library/custom-renders': ['renderHelper'] }, + code: ` + import { waitFor } from '${testingFramework}'; import { renderHelper } from 'somewhere-else'; await waitFor(() => { const { container } = renderHelper() }) `, - errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - settings: { 'testing-library/custom-renders': ['renderHelper'] }, - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + settings: { 'testing-library/custom-renders': ['renderHelper'] }, + code: ` + import { waitFor } from '${testingFramework}'; import { renderHelper } from 'somewhere-else'; let container; await waitFor(() => { ({ container } = renderHelper()) }) `, - errors: [{ line: 6, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 6, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => result = render()) `, - errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => (a = 5, result = render())) `, - errors: [{ line: 3, column: 30, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 3, column: 30, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; const { rerender } = render() await waitFor(() => rerender()) `, - errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor, render } from '@testing-library/react'; + errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor, render } from '${testingFramework}'; await waitFor(() => render()) `, - errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - const { rerender } = render() - await waitFor(() => rerender()) - `, - errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => renderHelper()) `, - errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; import { render } from 'somewhere-else'; await waitFor(() => render()) `, - errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - settings: { 'testing-library/utils-module': '~/test-utils' }, - code: ` + errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }], + } as const, + ]), + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` import { waitFor, render } from '~/test-utils'; await waitFor(() => render()) `, - errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - settings: { 'testing-library/custom-renders': ['renderWrapper'] }, - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + settings: { 'testing-library/custom-renders': ['renderWrapper'] }, + code: ` + import { waitFor } from '${testingFramework}'; import { renderWrapper } from 'somewhere-else'; await waitFor(() => renderWrapper()) `, - errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { render() }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { const { container } = render() }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { result = render() }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { const a = 5, { container } = render() }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; const { rerender } = render() await waitFor(() => { rerender() }) `, - errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { render() fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [ - { line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }, - { line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }, - ], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [ + { line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }, + { line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }, + ], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { render() userEvent.click(button) }) `, - errors: [ - { line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }, - { line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }, - ], - }, - // fireEvent - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [ + { line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }, + { line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }, + ], + } as const, + ]), + // fireEvent + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => fireEvent.keyDown(input, {key: 'ArrowDown'})) `, - errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - settings: { 'testing-library/utils-module': '~/test-utils' }, - code: ` + errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], + }) as const + ), + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` import { waitFor, fireEvent } from '~/test-utils'; await waitFor(() => fireEvent.keyDown(input, {key: 'ArrowDown'})) `, - errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor, fireEvent as renamedFireEvent } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor, fireEvent as renamedFireEvent } from '${testingFramework}'; await waitFor(() => { renamedFireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - settings: { 'testing-library/utils-module': '~/test-utils' }, - code: ` - import { waitFor, fireEvent } from '~/test-utils'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + ]), + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` + import { waitFor, fireEvent } from '~/test-utils'; await waitFor(() => { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { expect(b).toEqual('b') fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { fireEvent.keyDown(input, {key: 'ArrowDown'}) expect(b).toEqual('b') }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(function() { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(function() { expect(b).toEqual('b') fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(function() { fireEvent.keyDown(input, {key: 'ArrowDown'}) expect(b).toEqual('b') }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - // userEvent - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + ]), + // userEvent + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => userEvent.click(button)) `, - errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { userEvent.click(button) }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; - import renamedUserEvent from '@testing-library/user-event' + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; + import renamedUserEvent from '@testing-library/user-event' await waitFor(() => { renamedUserEvent.click(button) }) `, - errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - settings: { 'testing-library/utils-module': '~/test-utils' }, - code: ` + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + ]), + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` import { waitFor } from '~/test-utils'; - import userEvent from '@testing-library/user-event' + import userEvent from '@testing-library/user-event' await waitFor(() => { userEvent.click(); }) `, - errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { expect(b).toEqual('b') userEvent.click(button) }) `, - errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(() => { userEvent.click(button) expect(b).toEqual('b') }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(function() { userEvent.click(button) }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(function() { expect(b).toEqual('b') userEvent.click(button) }) `, - errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, - { - code: ` - import { waitFor } from '@testing-library/react'; + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + code: ` + import { waitFor } from '${testingFramework}'; await waitFor(function() { userEvent.click(button) expect(b).toEqual('b') }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], - }, + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + // Issue #500, https://github.com/testing-library/eslint-plugin-testing-library/issues/500 + code: ` + import { waitFor } from '${testingFramework}'; + waitFor(function() { + userEvent.click(button) + expect(b).toEqual('b') + }).then(() => { + userEvent.click(button) // Side effects are allowed inside .then() + expect(b).toEqual('b') + }) + `, + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + } as const, + { + // Issue #500, https://github.com/testing-library/eslint-plugin-testing-library/issues/500 + code: ` + import { waitFor } from '${testingFramework}'; + waitFor(function() { + userEvent.click(button) + expect(b).toEqual('b') + }).then(() => { + userEvent.click(button) // Side effects are allowed inside .then() + expect(b).toEqual('b') + await waitFor(() => { + fireEvent.keyDown(input, {key: 'ArrowDown'}) // But not if there is a another waitFor with side effects inside the .then() + expect(b).toEqual('b') + }) + }) + `, + errors: [ + { line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }, + { line: 10, column: 13, messageId: 'noSideEffectsWaitFor' }, + ], + } as const, + ]), - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: `// all mixed + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// all mixed import { waitFor, fireEvent as renamedFireEvent, screen } from '~/test-utils'; import userEvent from '@testing-library/user-event' import { fireEvent } from 'somewhere-else' - + test('check all mixed', async () => { const button = await screen.findByRole('button') await waitFor(() => { @@ -713,10 +787,48 @@ ruleTester.run(RULE_NAME, rule, { }) }) `, - errors: [ - { line: 9, column: 13, messageId: 'noSideEffectsWaitFor' }, - { line: 12, column: 13, messageId: 'noSideEffectsWaitFor' }, - ], - }, - ], + errors: [ + { line: 9, column: 13, messageId: 'noSideEffectsWaitFor' }, + { line: 12, column: 13, messageId: 'noSideEffectsWaitFor' }, + ], + }, + // side effects (userEvent, fireEvent or render) in variable declarations + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + // Issue #368, https://github.com/testing-library/eslint-plugin-testing-library/issues/368 + code: ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + await waitFor(() => { + const a = userEvent.click(button); + const b = fireEvent.click(button); + const wrapper = render(); + }) + `, + errors: [ + { line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }, + { line: 6, column: 11, messageId: 'noSideEffectsWaitFor' }, + { line: 7, column: 11, messageId: 'noSideEffectsWaitFor' }, + ], + } as const, + ]), + + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap>( + (testingFramework) => [ + { + code: ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + + it("some test", async () => { + await waitFor(async () => { + await fireEvent.click(screen.getByTestId("something")); + }); + }); + `, + errors: [{ line: 7, column: 13, messageId: 'noSideEffectsWaitFor' }], + }, + ] + ), + ], }); diff --git a/tests/lib/rules/no-wait-for-snapshot.test.ts b/tests/lib/rules/no-wait-for-snapshot.test.ts index b6f3e474..03eff8f4 100644 --- a/tests/lib/rules/no-wait-for-snapshot.test.ts +++ b/tests/lib/rules/no-wait-for-snapshot.test.ts @@ -4,68 +4,78 @@ import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + ruleTester.run(RULE_NAME, rule, { - valid: [ - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; - test('snapshot calls outside of ${asyncUtil} are valid', () => { - expect(foo).toMatchSnapshot() - await ${asyncUtil}(() => expect(foo).toBeDefined()) - expect(foo).toMatchInlineSnapshot() - }) - `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; - test('snapshot calls outside of ${asyncUtil} are valid', () => { - expect(foo).toMatchSnapshot() - await ${asyncUtil}(() => { - expect(foo).toBeDefined() + valid: [ + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('snapshot calls outside of ${asyncUtil} are valid', () => { + expect(foo).toMatchSnapshot() + await ${asyncUtil}(() => expect(foo).toBeDefined()) + expect(foo).toMatchInlineSnapshot() }) - expect(foo).toMatchInlineSnapshot() - }) - `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import * as asyncUtils from '@testing-library/dom'; - test('snapshot calls outside of ${asyncUtil} are valid', () => { - expect(foo).toMatchSnapshot() - await asyncUtils.${asyncUtil}(() => expect(foo).toBeDefined()) - expect(foo).toMatchInlineSnapshot() - }) - `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import * as asyncUtils from '@testing-library/dom'; - test('snapshot calls outside of ${asyncUtil} are valid', () => { - expect(foo).toMatchSnapshot() - await asyncUtils.${asyncUtil}(() => { - expect(foo).toBeDefined() + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('snapshot calls outside of ${asyncUtil} are valid', () => { + expect(foo).toMatchSnapshot() + await ${asyncUtil}(() => { + expect(foo).toBeDefined() + }) + expect(foo).toMatchInlineSnapshot() }) - expect(foo).toMatchInlineSnapshot() - }) - `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import * as asyncUtils from '${testingFramework}'; + test('snapshot calls outside of ${asyncUtil} are valid', () => { + expect(foo).toMatchSnapshot() + await asyncUtils.${asyncUtil}(() => expect(foo).toBeDefined()) + expect(foo).toMatchInlineSnapshot() + }) + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import * as asyncUtils from '${testingFramework}'; + test('snapshot calls outside of ${asyncUtil} are valid', () => { + expect(foo).toMatchSnapshot() + await asyncUtils.${asyncUtil}(() => { + expect(foo).toBeDefined() + }) + expect(foo).toMatchInlineSnapshot() + }) + `, + })), + ]), + ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { ${asyncUtil} } from 'some-other-library'; test('aggressive reporting disabled - snapshot calls within ${asyncUtil} not related to Testing Library are valid', async () => { await ${asyncUtil}(() => expect(foo).toMatchSnapshot()); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { ${asyncUtil} } from 'some-other-library'; test('(alt) aggressive reporting disabled - snapshot calls within ${asyncUtil} not related to Testing Library are valid', async () => { await ${asyncUtil}(() => { @@ -74,23 +84,23 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import * as asyncUtils from 'some-other-library'; test('aggressive reporting disabled - snapshot calls within ${asyncUtil} from wildcard import not related to Testing Library are valid', async () => { await asyncUtils.${asyncUtil}(() => expect(foo).toMatchSnapshot()); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import * as asyncUtils from 'some-other-library'; test('(alt) aggressive reporting disabled - snapshot calls within ${asyncUtil} from wildcard import not related to Testing Library are valid', async () => { await asyncUtils.${asyncUtil}(() => { @@ -99,23 +109,23 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { ${asyncUtil} } from 'some-other-library'; test('aggressive reporting disabled - inline snapshot calls within ${asyncUtil} import not related to Testing Library are valid', async () => { await ${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { ${asyncUtil} } from 'some-other-library'; test('(alt) aggressive reporting disabled - inline snapshot calls within ${asyncUtil} import not related to Testing Library are valid', async () => { await ${asyncUtil}(() => { @@ -124,23 +134,23 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import * as asyncUtils from 'some-other-library'; test('aggressive reporting disabled - inline snapshot calls within ${asyncUtil} from wildcard import not related to Testing Library are valid', async () => { await asyncUtils.${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); }); `, - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import * as asyncUtils from 'some-other-library'; test('(alt) aggressive reporting disabled - inline snapshot calls within ${asyncUtil} from wildcard import not related to Testing Library are valid', async () => { await asyncUtils.${asyncUtil}(() => { @@ -149,168 +159,168 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - })), - ], - invalid: [ - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + })), + ], + invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await ${asyncUtil}(() => expect(foo).toMatchSnapshot()); }); `, - errors: [ - { - line: 4, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 36 + asyncUtil.length, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 36 + asyncUtil.length, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await ${asyncUtil}(() => { expect(foo).toMatchSnapshot() }); }); `, - errors: [ - { - line: 5, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 27, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import * as asyncUtils from '@testing-library/dom'; + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import * as asyncUtils from '${testingFramework}'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await asyncUtils.${asyncUtil}(() => expect(foo).toMatchSnapshot()); }); `, - errors: [ - { - line: 4, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 47 + asyncUtil.length, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import * as asyncUtils from '@testing-library/dom'; + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 47 + asyncUtil.length, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import * as asyncUtils from '${testingFramework}'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await asyncUtils.${asyncUtil}(() => { expect(foo).toMatchSnapshot() }); }); `, - errors: [ - { - line: 5, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 27, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await ${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); }); `, - errors: [ - { - line: 4, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 36 + asyncUtil.length, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import { ${asyncUtil} } from '@testing-library/dom'; + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 36 + asyncUtil.length, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await ${asyncUtil}(() => { expect(foo).toMatchInlineSnapshot() }); }); `, - errors: [ - { - line: 5, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 27, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import * as asyncUtils from '@testing-library/dom'; + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import * as asyncUtils from '${testingFramework}'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await asyncUtils.${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); }); `, - errors: [ - { - line: 4, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 47 + asyncUtil.length, - }, - ], - } as const) - ), - ...ASYNC_UTILS.map( - (asyncUtil) => - ({ - code: ` - import * as asyncUtils from '@testing-library/dom'; + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 47 + asyncUtil.length, + }, + ], + }) as const + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` + import * as asyncUtils from '${testingFramework}'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await asyncUtils.${asyncUtil}(() => { expect(foo).toMatchInlineSnapshot() }); }); `, - errors: [ - { - line: 5, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 27, - }, - ], - } as const) - ), - ], + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], + }) as const + ), + ]), }); diff --git a/tests/lib/rules/prefer-explicit-assert.test.ts b/tests/lib/rules/prefer-explicit-assert.test.ts index 08dd5fb8..8cc09614 100644 --- a/tests/lib/rules/prefer-explicit-assert.test.ts +++ b/tests/lib/rules/prefer-explicit-assert.test.ts @@ -7,283 +7,283 @@ const ruleTester = createRuleTester(); const COMBINED_QUERIES_METHODS = [...ALL_QUERIES_METHODS, 'ByIcon']; ruleTester.run(RULE_NAME, rule, { - valid: [ - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `get${queryMethod}('Hello')`, - settings: { - 'testing-library/utils-module': 'test-utils', - }, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `get${queryMethod}`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + valid: [ + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `get${queryMethod}('Hello')`, + settings: { + 'testing-library/utils-module': 'test-utils', + }, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `get${queryMethod}`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` const utils = render() utils.get${queryMethod} `, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `screen.get${queryMethod}`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `expect(get${queryMethod}('foo')).toBeDefined()`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `screen.get${queryMethod}`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).toBeDefined()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` const utils = render() expect(utils.get${queryMethod}('foo')).toBeDefined() `, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `expect(screen.get${queryMethod}('foo')).toBeDefined()`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `expect(getBy${queryMethod}('foo').bar).toBeInTheDocument()`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(screen.get${queryMethod}('foo')).toBeDefined()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(getBy${queryMethod}('foo').bar).toBeInTheDocument()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` async () => { await waitForElement(() => get${queryMethod}('foo')) } `, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `fireEvent.click(get${queryMethod}('bar'));`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `const quxElement = get${queryMethod}('qux')`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `fireEvent.click(get${queryMethod}('bar'));`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const quxElement = get${queryMethod}('qux')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` async () => { const quxElement = await find${queryMethod}('qux') }`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` async () => { expect(await find${queryMethod}('qux')).toBeInTheDocument(); }`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` async () => { await find${queryMethod}('foo') }`, - options: [ - { - includeFindQueries: false, - }, - ], - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `const quxElement = find${queryMethod}('qux')`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `const quxElement = screen.find${queryMethod}('qux')`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + options: [ + { + includeFindQueries: false, + }, + ], + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const quxElement = find${queryMethod}('qux')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const quxElement = screen.find${queryMethod}('qux')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` async () => { const quxElement = await screen.find${queryMethod}('qux') }`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` function findBySubmit() { return screen.find${queryMethod}('foo') }`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` function findBySubmit() { return find${queryMethod}('foo') }`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` () => { return screen.find${queryMethod}('foo') }`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` () => { return find${queryMethod}('foo') }`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` () => screen.find${queryMethod}('foo')`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` () => find${queryMethod}('foo')`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `() => { return get${queryMethod}('foo') }`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `function bar() { return get${queryMethod}('foo') }`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `const { get${queryMethod} } = render()`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `it('test', () => { const { get${queryMethod} } = render() })`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `it('test', () => { const [ get${queryMethod} ] = render() })`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `const a = [ get${queryMethod}('foo') ]`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `const a = { foo: get${queryMethod}('bar') }`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `query${queryMethod}("foo")`, - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `() => { return get${queryMethod}('foo') }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `function bar() { return get${queryMethod}('foo') }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const { get${queryMethod} } = render()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `it('test', () => { const { get${queryMethod} } = render() })`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `it('test', () => { const [ get${queryMethod} ] = render() })`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const a = [ get${queryMethod}('foo') ]`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const a = { foo: get${queryMethod}('bar') }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `query${queryMethod}("foo")`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` expect(get${queryMethod}('foo')).toBeTruthy() fireEvent.click(get${queryMethod}('bar')); `, - options: [ - { - assertion: 'toBeTruthy', - }, - ], - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `expect(get${queryMethod}('foo')).toBeEnabled()`, - options: [ - { - assertion: 'toBeInTheDocument', - }, - ], - })), - { - // https://github.com/testing-library/eslint-plugin-testing-library/issues/475 - code: ` + options: [ + { + assertion: 'toBeTruthy', + }, + ], + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).toBeEnabled()`, + options: [ + { + assertion: 'toBeInTheDocument', + }, + ], + })), + { + // https://github.com/testing-library/eslint-plugin-testing-library/issues/475 + code: ` // incomplete expect statement should be ignored expect('something'); expect(getByText('foo')); `, - options: [ - { - assertion: 'toBeInTheDocument', - }, - ], - }, - ], - invalid: [ - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - code: `get${queryMethod}('foo')`, - errors: [ - { - messageId: 'preferExplicitAssert', - data: { - queryType: 'getBy*', - }, - }, - ], - } as const) - ), - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - code: `find${queryMethod}('foo')`, - errors: [ - { - messageId: 'preferExplicitAssert', - data: { queryType: 'findBy*' }, - }, - ], - } as const) - ), - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - code: `screen.find${queryMethod}('foo')`, - errors: [ - { - messageId: 'preferExplicitAssert', - data: { queryType: 'findBy*' }, - }, - ], - } as const) - ), - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - code: ` + options: [ + { + assertion: 'toBeInTheDocument', + }, + ], + }, + ], + invalid: [ + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `get${queryMethod}('foo')`, + errors: [ + { + messageId: 'preferExplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `find${queryMethod}('foo')`, + errors: [ + { + messageId: 'preferExplicitAssert', + data: { queryType: 'findBy*' }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `screen.find${queryMethod}('foo')`, + errors: [ + { + messageId: 'preferExplicitAssert', + data: { queryType: 'findBy*' }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` async () => { await screen.find${queryMethod}('foo') } `, - errors: [ - { - messageId: 'preferExplicitAssert', - data: { queryType: 'findBy*' }, - }, - ], - } as const) - ), - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - code: ` + errors: [ + { + messageId: 'preferExplicitAssert', + data: { queryType: 'findBy*' }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` async () => { await find${queryMethod}('foo') } `, - errors: [ - { - messageId: 'preferExplicitAssert', - data: { queryType: 'findBy*' }, - }, - ], - } as const) - ), - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - code: ` + errors: [ + { + messageId: 'preferExplicitAssert', + data: { queryType: 'findBy*' }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` const utils = render() utils.get${queryMethod}('foo') `, - errors: [ - { - messageId: 'preferExplicitAssert', - line: 3, - column: 15, - data: { - queryType: 'getBy*', - }, - }, - ], - } as const) - ), - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - code: `screen.get${queryMethod}('foo')`, - errors: [ - { - messageId: 'preferExplicitAssert', - line: 1, - column: 8, - data: { - queryType: 'getBy*', - }, - }, - ], - } as const) - ), - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - code: ` + errors: [ + { + messageId: 'preferExplicitAssert', + line: 3, + column: 15, + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `screen.get${queryMethod}('foo')`, + errors: [ + { + messageId: 'preferExplicitAssert', + line: 1, + column: 8, + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` () => { get${queryMethod}('foo') doSomething() @@ -292,102 +292,94 @@ ruleTester.run(RULE_NAME, rule, { const quxElement = get${queryMethod}('qux') } `, - errors: [ - { - messageId: 'preferExplicitAssert', - line: 3, - data: { - queryType: 'getBy*', - }, - }, - { - messageId: 'preferExplicitAssert', - line: 6, - data: { - queryType: 'getBy*', - }, - }, - ], - } as const) - ), - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + messageId: 'preferExplicitAssert', + line: 3, + data: { + queryType: 'getBy*', + }, + }, + { + messageId: 'preferExplicitAssert', + line: 6, + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import "test-utils" getBy${queryMethod}("Hello") `, - errors: [ - { - messageId: 'preferExplicitAssert', - data: { - queryType: 'getBy*', - }, - }, - ], - } as const) - ), - { - code: `getByIcon('foo')`, // custom `getBy` query extended through options - errors: [ - { - messageId: 'preferExplicitAssert', - }, - ], - }, - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - code: `expect(get${queryMethod}('foo')).toBeDefined()`, - options: [ - { - assertion: 'toBeInTheDocument', - }, - ], - errors: [ - { - messageId: 'preferExplicitAssertAssertion', - data: { assertion: 'toBeInTheDocument' }, - }, - ], - } as const) - ), - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - code: `expect(get${queryMethod}('foo')).not.toBeNull()`, - options: [ - { - assertion: 'toBeInTheDocument', - }, - ], - errors: [ - { - messageId: 'preferExplicitAssertAssertion', - data: { assertion: 'toBeInTheDocument' }, - }, - ], - } as const) - ), - ...COMBINED_QUERIES_METHODS.map( - (queryMethod) => - ({ - code: `expect(get${queryMethod}('foo')).not.toBeFalsy()`, - options: [ - { - assertion: 'toBeInTheDocument', - }, - ], - errors: [ - { - messageId: 'preferExplicitAssertAssertion', - data: { assertion: 'toBeInTheDocument' }, - }, - ], - } as const) - ), - ], + errors: [ + { + messageId: 'preferExplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(get${queryMethod}('foo')).toBeDefined()`, + options: [ + { + assertion: 'toBeInTheDocument', + }, + ], + errors: [ + { + messageId: 'preferExplicitAssertAssertion', + data: { assertion: 'toBeInTheDocument' }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(get${queryMethod}('foo')).not.toBeNull()`, + options: [ + { + assertion: 'toBeInTheDocument', + }, + ], + errors: [ + { + messageId: 'preferExplicitAssertAssertion', + data: { assertion: 'toBeInTheDocument' }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(get${queryMethod}('foo')).not.toBeFalsy()`, + options: [ + { + assertion: 'toBeInTheDocument', + }, + ], + errors: [ + { + messageId: 'preferExplicitAssertAssertion', + data: { assertion: 'toBeInTheDocument' }, + }, + ], + }) as const + ), + ], }); diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index aaafdaef..b4728c15 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -1,103 +1,116 @@ -import { TSESLint } from '@typescript-eslint/experimental-utils'; +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; import rule, { - WAIT_METHODS, - RULE_NAME, - getFindByQueryVariant, - MessageIds, + RULE_NAME, + getFindByQueryVariant, + MessageIds, } from '../../../lib/rules/prefer-find-by'; import { - ASYNC_QUERIES_COMBINATIONS, - SYNC_QUERIES_COMBINATIONS, + ASYNC_QUERIES_COMBINATIONS, + SYNC_QUERIES_COMBINATIONS, } from '../../../lib/utils'; import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + function buildFindByMethod(queryMethod: string) { - return `${getFindByQueryVariant(queryMethod)}${queryMethod.split('By')[1]}`; + return `${getFindByQueryVariant(queryMethod)}${queryMethod.split('By')[1]}`; } function createScenario< - T extends - | TSESLint.InvalidTestCase - | TSESLint.ValidTestCase<[]> + T extends InvalidTestCase | ValidTestCase<[]>, >(callback: (waitMethod: string, queryMethod: string) => T) { - return WAIT_METHODS.reduce( - (acc: T[], waitMethod) => - acc.concat( - SYNC_QUERIES_COMBINATIONS.map((queryMethod) => - callback(waitMethod, queryMethod) - ) - ), - [] - ); + return SYNC_QUERIES_COMBINATIONS.map((queryMethod) => + callback('waitFor', queryMethod) + ); } ruleTester.run(RULE_NAME, rule, { - valid: [ - ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` + valid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` it('tests', async () => { - const { ${queryMethod} } = setup() + const { ${queryMethod} } = setup() // implicitly using ${testingFramework} const submitButton = await ${queryMethod}('foo') }) `, - })), - ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` - import {screen} from '@testing-library/foo'; + })), + ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {screen} from '${testingFramework}'; it('tests', async () => { const submitButton = await screen.${queryMethod}('foo') }) `, - })), - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` - import {waitForElementToBeRemoved} from '@testing-library/foo'; + })), + ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + await waitFor(async () => { + const button = screen.${queryMethod}("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) + }) + `, + })), + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {waitForElementToBeRemoved} from '${testingFramework}'; it('tests', async () => { await waitForElementToBeRemoved(() => ${queryMethod}(baz)) }) `, - })), - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` - import {waitFor} from '@testing-library/foo'; + })), + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {waitFor} from '${testingFramework}'; it('tests', async () => { await waitFor(function() { return ${queryMethod}('baz', { name: 'foo' }) }) }) `, - })), - { - code: ` - import {waitFor} from '@testing-library/foo'; + })), + { + code: ` + import {waitFor} from '${testingFramework}'; it('tests', async () => { await waitFor(() => myCustomFunction()) }) `, - }, - { - code: ` - import {waitFor} from '@testing-library/foo'; + }, + { + code: ` + import {waitFor} from '${testingFramework}'; it('tests', async () => { await waitFor(customFunctionReference) }) `, - }, - { - code: ` - import {waitForElementToBeRemoved} from '@testing-library/foo'; + }, + { + code: ` + import {waitForElementToBeRemoved} from '${testingFramework}'; it('tests', async () => { const { container } = render() await waitForElementToBeRemoved(container.querySelector('foo')) }) `, - }, - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` - import {waitFor} from '@testing-library/foo'; + }, + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {waitFor} from '${testingFramework}'; it('tests', async () => { await waitFor(() => { foo() @@ -105,579 +118,632 @@ ruleTester.run(RULE_NAME, rule, { }) }) `, - })), - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` - import {screen, waitFor} from '@testing-library/foo'; + })), + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {screen, waitFor} from '${testingFramework}'; it('tests', async () => { await waitFor(() => expect(screen.${queryMethod}('baz')).toBeDisabled()); }) `, - })), - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` - import {screen, waitFor} from '@testing-library/foo'; + })), + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {screen, waitFor} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod} } = render() await waitFor(() => expect(${queryMethod}('baz')).toBeDisabled()); }) `, - })), - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` - import {waitFor} from '@testing-library/foo'; + })), + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {waitFor} from '${testingFramework}'; it('tests', async () => { await waitFor(() => expect(screen.${queryMethod}('baz')).not.toBeInTheDocument()); }) `, - })), - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` - import {waitFor} from '@testing-library/foo'; + })), + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {waitFor} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod} } = render() await waitFor(() => expect(${queryMethod}('baz')).not.toBeInTheDocument()); }) `, - })), - { - code: ` - import {waitFor} from '@testing-library/foo'; + })), + { + code: ` + import {waitFor} from '${testingFramework}'; it('tests', async () => { await waitFor(); - await wait(); }) `, - }, - ], - invalid: [ - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}, screen} from '@testing-library/foo'; + }, + { + code: ` + import {screen, waitFor} from '${testingFramework}'; + it('tests', async () => { + await waitFor(() => expect(screen.querySelector('baz')).toBeInTheDocument()); + }) + `, + }, + { + code: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + const { container } = render() + await waitFor(() => expect(container.querySelector('baz')).toBeInTheDocument()); + }) + `, + }, + { + code: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + await waitFor(async () => { + const button = await foo("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) + }) + `, + }, + ]), + invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}, screen} from '${testingFramework}'; it('tests', async () => { const submitButton = await ${waitMethod}(() => screen.${queryMethod}('foo', { name: 'baz' })) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}, screen} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}, screen} from '${testingFramework}'; it('tests', async () => { const submitButton = await screen.${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - // // this scenario verifies it works when the render function is defined in another scope - ...WAIT_METHODS.map( - (waitMethod: string) => - ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + // // this scenario verifies it works when the render function is defined in another scope + { + code: ` + import { waitFor } from '${testingFramework}'; const { getByText, queryByLabelText, findAllByRole } = customRender() it('tests', async () => { - const submitButton = await ${waitMethod}(() => getByText('baz', { name: 'button' })) + const submitButton = await waitFor(() => getByText('baz', { name: 'button' })) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'Text', - prevQuery: 'getByText', - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'Text', + prevQuery: 'getByText', + }, + }, + ], + output: ` + import { waitFor } from '${testingFramework}'; const { getByText, queryByLabelText, findAllByRole, findByText } = customRender() it('tests', async () => { const submitButton = await findByText('baz', { name: 'button' }) }) `, - } as const) - ), - // // this scenario verifies when findBy* were already defined (because it was used elsewhere) - ...WAIT_METHODS.map( - (waitMethod: string) => - ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + }, + // // this scenario verifies when findBy* were already defined (because it was used elsewhere) + { + code: ` + import { waitFor } from '${testingFramework}'; const { getAllByRole, findAllByRole } = customRender() it('tests', async () => { - const submitButton = await ${waitMethod}(() => getAllByRole('baz', { name: 'button' })) + const submitButton = await waitFor(() => getAllByRole('baz', { name: 'button' })) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findAllBy', - queryMethod: 'Role', - prevQuery: 'getAllByRole', - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findAllBy', + queryMethod: 'Role', + prevQuery: 'getAllByRole', + }, + }, + ], + output: ` + import { waitFor } from '${testingFramework}'; const { getAllByRole, findAllByRole } = customRender() it('tests', async () => { const submitButton = await findAllByRole('baz', { name: 'button' }) }) `, - } as const) - ), - // invalid code, as we need findBy* to be defined somewhere, but required for getting 100% coverage - { - code: `const submitButton = await waitFor(() => getByText('baz', { name: 'button' }))`, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'Text', - prevQuery: 'getByText', - waitForMethodName: 'waitFor', - }, - }, - ], - output: `const submitButton = await findByText('baz', { name: 'button' })`, - }, - // this code would be invalid too, as findByRole is not defined anywhere. - { - code: ` - const getByRole = render().getByRole + }, + // invalid code, as we need findBy* to be defined somewhere, but required for getting 100% coverage + { + code: `const submitButton = await waitFor(() => getByText('baz', { name: 'button' })) // implicitly using ${testingFramework}`, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'Text', + prevQuery: 'getByText', + waitForMethodName: 'waitFor', + }, + }, + ], + output: `const submitButton = await findByText('baz', { name: 'button' }) // implicitly using ${testingFramework}`, + }, + // this code would be invalid too, as findByRole is not defined anywhere. + { + code: ` + const getByRole = render().getByRole // implicitly using ${testingFramework} const submitButton = await waitFor(() => getByRole('baz', { name: 'button' })) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'Role', - prevQuery: 'getByRole', - waitForMethodName: 'waitFor', - }, - }, - ], - output: ` - const getByRole = render().getByRole + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'Role', + prevQuery: 'getByRole', + waitForMethodName: 'waitFor', + }, + }, + ], + output: ` + const getByRole = render().getByRole // implicitly using ${testingFramework} const submitButton = await findByRole('baz', { name: 'button' }) `, - }, - // custom query triggers the error but there is no fix - so output is the same - ...WAIT_METHODS.map( - (waitMethod: string) => - ({ - code: ` - import {${waitMethod},render} from '@testing-library/foo'; - it('tests', async () => { - const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) - }) - `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'CustomQuery', - prevQuery: 'getByCustomQuery', - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod},render} from '@testing-library/foo'; + }, + // custom query triggers the error but there is no fix - so output is the same + { + code: ` + import { waitFor, render} from '${testingFramework}'; it('tests', async () => { const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) + const submitButton = await waitFor(() => getByCustomQuery('baz')) }) `, - } as const) - ), - // custom query triggers the error but there is no fix - so output is the same - ...WAIT_METHODS.map( - (waitMethod: string) => - ({ - code: ` - import {${waitMethod},render,screen} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'CustomQuery', + prevQuery: 'getByCustomQuery', + }, + }, + ], + output: null, + }, + // custom query triggers the error but there is no fix - so output is the same + { + code: ` + import {waitFor,render,screen} from '${testingFramework}'; it('tests', async () => { const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) + const submitButton = await waitFor(() => screen.getByCustomQuery('baz')) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'CustomQuery', - prevQuery: 'getByCustomQuery', - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod},render,screen} from '@testing-library/foo'; - it('tests', async () => { - const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) - }) - `, - } as const) - ), - // presence matchers - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'CustomQuery', + prevQuery: 'getByCustomQuery', + }, + }, + ], + output: null, + }, + // presence matchers + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod} } = render() const submitButton = await ${waitMethod}(() => ${queryMethod}('foo', { name: 'baz' })) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() const submitButton = await ${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod} } = render() const submitButton = await ${waitMethod}(() => expect(${queryMethod}('foo', { name: 'baz' })).toBeInTheDocument()) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() const submitButton = await ${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod} } = render() const submitButton = await ${waitMethod}(() => expect(${queryMethod}('foo', { name: 'baz' })).toBeDefined()) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() const submitButton = await ${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod} } = render() const submitButton = await ${waitMethod}(() => expect(${queryMethod}('foo', { name: 'baz' })).not.toBeNull()) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() const submitButton = await ${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const {${queryMethod}} = render() const submitButton = await ${waitMethod}(() => expect(${queryMethod}('foo', { name: 'baz' })).not.toBeNull()) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const {${queryMethod}, ${buildFindByMethod(queryMethod)}} = render() const submitButton = await ${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod} } = render() const submitButton = await ${waitMethod}(() => expect(${queryMethod}('foo', { name: 'baz' })).toBeTruthy()) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() const submitButton = await ${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod} } = render() const submitButton = await ${waitMethod}(() => expect(${queryMethod}('foo', { name: 'baz' })).not.toBeFalsy()) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() const submitButton = await ${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const submitButton = await ${waitMethod}(() => expect(screen.${queryMethod}('foo', { name: 'baz' })).toBeInTheDocument()) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const submitButton = await screen.${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const submitButton = await ${waitMethod}(() => expect(screen.${queryMethod}('foo', { name: 'baz' })).toBeDefined()) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const submitButton = await screen.${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const submitButton = await ${waitMethod}(() => expect(screen.${queryMethod}('foo', { name: 'baz' })).not.toBeNull()) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const submitButton = await screen.${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const submitButton = await ${waitMethod}(() => expect(screen.${queryMethod}('foo', { name: 'baz' })).toBeTruthy()) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const submitButton = await screen.${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}} from '@testing-library/foo'; + })), + ...createScenario((waitMethod, queryMethod) => ({ + code: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const submitButton = await ${waitMethod}(() => expect(screen.${queryMethod}('foo', { name: 'baz' })).not.toBeFalsy()) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { const submitButton = await screen.${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + queryMethod + )}('foo', { name: 'baz' }) }) `, - })), - ], + })), + // Issue #579, https://github.com/testing-library/eslint-plugin-testing-library/issues/579 + // findBy can have two sets of options: await screen.findByText('text', queryOptions, waitForOptions) + ...createScenario((waitMethod, queryMethod) => ({ + code: `import {${waitMethod}} from '${testingFramework}'; + const button = await ${waitMethod}(() => screen.${queryMethod}('Count is: 0'), { timeout: 100, interval: 200 }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: `import {${waitMethod}} from '${testingFramework}'; + const button = await screen.${buildFindByMethod( + queryMethod + )}('Count is: 0', { timeout: 100, interval: 200 }) + `, + })), + ...ASYNC_QUERIES_COMBINATIONS.map>( + (queryMethod) => ({ + code: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + await waitFor(async () => { + const button = await screen.${queryMethod}("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: 'waitFor', + }, + }, + ], + output: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + const button = await screen.${queryMethod}("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) + `, + }) + ), + ]), }); diff --git a/tests/lib/rules/prefer-implicit-assert.test.ts b/tests/lib/rules/prefer-implicit-assert.test.ts new file mode 100644 index 00000000..c73ac0c7 --- /dev/null +++ b/tests/lib/rules/prefer-implicit-assert.test.ts @@ -0,0 +1,552 @@ +import rule, { RULE_NAME } from '../../../lib/rules/prefer-implicit-assert'; +import { ALL_QUERIES_METHODS } from '../../../lib/utils'; +import { createRuleTester } from '../test-utils'; + +const ruleTester = createRuleTester(); + +const COMBINED_QUERIES_METHODS = [...ALL_QUERIES_METHODS, 'ByIcon']; + +ruleTester.run(RULE_NAME, rule, { + valid: [ + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `await find${queryMethod}('qux');`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `screen.find${queryMethod}('foo')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `await screen.find${queryMethod}('foo')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const utils = render(); + await utils.find${queryMethod}('foo')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `get${queryMethod}('qux');`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `screen.get${queryMethod}('foo')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const utils = render(); + utils.get${queryMethod}('foo')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(query${queryMethod}('qux')).toBeInTheDocument();`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(query${queryMethod}('qux')).not.toBeInTheDocument();`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const something = await find${queryMethod}('qux');`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const something = get${queryMethod}('qux');`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const something = query${queryMethod}('qux');`, + })), + ], + invalid: [ + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(await find${queryMethod}('qux')).toBeInTheDocument();`, + errors: [ + { + line: 1, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(await find${queryMethod}('qux')).toBeTruthy();`, + errors: [ + { + line: 1, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(await find${queryMethod}('qux')).toBeDefined();`, + errors: [ + { + line: 1, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(await find${queryMethod}('qux')).not.toBeNull();`, + errors: [ + { + line: 1, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(await find${queryMethod}('qux')).not.toBeFalsy();`, + errors: [ + { + line: 1, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(await screen.find${queryMethod}('qux')).toBeInTheDocument();`, + errors: [ + { + line: 1, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(await screen.find${queryMethod}('qux')).toBeTruthy();`, + errors: [ + { + line: 1, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(await screen.find${queryMethod}('qux')).toBeDefined();`, + errors: [ + { + line: 1, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(await screen.find${queryMethod}('qux')).not.toBeNull();`, + errors: [ + { + line: 1, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(await screen.find${queryMethod}('qux')).not.toBeFalsy();`, + errors: [ + { + line: 1, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + const utils = render(); + expect(await utils.find${queryMethod}('foo')).toBeInTheDocument();`, + errors: [ + { + line: 3, + column: 20, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + const utils = render(); + expect(await utils.find${queryMethod}('foo')).toBeTruthy();`, + errors: [ + { + line: 3, + column: 20, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + const utils = render(); + expect(await utils.find${queryMethod}('foo')).toBeDefined();`, + errors: [ + { + line: 3, + column: 20, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + const utils = render(); + expect(await utils.find${queryMethod}('foo')).not.toBeFalsy();`, + errors: [ + { + line: 3, + column: 20, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + const utils = render(); + expect(await utils.find${queryMethod}('foo')).not.toBeNull();`, + errors: [ + { + line: 3, + column: 20, + messageId: 'preferImplicitAssert', + data: { + queryType: 'findBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(get${queryMethod}('qux')).toBeInTheDocument();`, + errors: [ + { + line: 1, + column: 8, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(get${queryMethod}('qux')).toBeTruthy();`, + errors: [ + { + line: 1, + column: 8, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(get${queryMethod}('qux')).toBeDefined();`, + errors: [ + { + line: 1, + column: 8, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(get${queryMethod}('qux')).not.toBeNull();`, + errors: [ + { + line: 1, + column: 8, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(get${queryMethod}('qux')).not.toBeFalsy();`, + errors: [ + { + line: 1, + column: 8, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(screen.get${queryMethod}('qux')).toBeInTheDocument();`, + errors: [ + { + line: 1, + column: 8, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(screen.get${queryMethod}('qux')).toBeTruthy();`, + errors: [ + { + line: 1, + column: 8, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(screen.get${queryMethod}('qux')).toBeDefined();`, + errors: [ + { + line: 1, + column: 8, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(screen.get${queryMethod}('qux')).not.toBeNull();`, + errors: [ + { + line: 1, + column: 8, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(screen.get${queryMethod}('qux')).not.toBeFalsy();`, + errors: [ + { + line: 1, + column: 8, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + const utils = render(); + expect(utils.get${queryMethod}('foo')).toBeInTheDocument();`, + errors: [ + { + line: 3, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + const utils = render(); + expect(utils.get${queryMethod}('foo')).toBeTruthy();`, + errors: [ + { + line: 3, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + const utils = render(); + expect(utils.get${queryMethod}('foo')).toBeDefined();`, + errors: [ + { + line: 3, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + const utils = render(); + expect(utils.get${queryMethod}('foo')).not.toBeFalsy();`, + errors: [ + { + line: 3, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + const utils = render(); + expect(utils.get${queryMethod}('foo')).not.toBeNull();`, + errors: [ + { + line: 3, + column: 14, + messageId: 'preferImplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + }) as const + ), + ], +}); diff --git a/tests/lib/rules/prefer-presence-queries.test.ts b/tests/lib/rules/prefer-presence-queries.test.ts index 8ff037d4..72cbe4d5 100644 --- a/tests/lib/rules/prefer-presence-queries.test.ts +++ b/tests/lib/rules/prefer-presence-queries.test.ts @@ -1,8 +1,12 @@ -import { TSESLint } from '@typescript-eslint/experimental-utils'; +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; import rule, { - RULE_NAME, - MessageIds, + RULE_NAME, + MessageIds, + Options, } from '../../../lib/rules/prefer-presence-queries'; import { ALL_QUERIES_METHODS } from '../../../lib/utils'; import { createRuleTester } from '../test-utils'; @@ -13,681 +17,1558 @@ const getByQueries = ALL_QUERIES_METHODS.map((method) => `get${method}`); const getAllByQueries = ALL_QUERIES_METHODS.map((method) => `getAll${method}`); const queryByQueries = ALL_QUERIES_METHODS.map((method) => `query${method}`); const queryAllByQueries = ALL_QUERIES_METHODS.map( - (method) => `queryAll${method}` + (method) => `queryAll${method}` ); -type RuleValidTestCase = TSESLint.ValidTestCase<[]>; -type RuleInvalidTestCase = TSESLint.InvalidTestCase; +type RuleValidTestCase = ValidTestCase; +type RuleInvalidTestCase = InvalidTestCase; type AssertionFnParams = { - query: string; - matcher: string; - messageId: MessageIds; - shouldUseScreen?: boolean; + query: string; + matcher: string; + messageId: MessageIds; + shouldUseScreen?: boolean; + assertionType: keyof Options[number]; }; -const getValidAssertion = ({ - query, - matcher, - shouldUseScreen = false, +const getValidAssertions = ({ + query, + matcher, + shouldUseScreen = false, + assertionType, +}: Omit): RuleValidTestCase[] => { + const finalQuery = shouldUseScreen ? `screen.${query}` : query; + const code = `expect(${finalQuery}('Hello'))${matcher}`; + return [ + { + code, + }, + { + code, + options: [ + { + [assertionType]: true, + [assertionType === 'absence' ? 'presence' : 'absence']: false, + }, + ], + }, + { + code, + options: [ + { + presence: false, + absence: false, + }, + ], + }, + ]; +}; + +const getDisabledValidAssertion = ({ + query, + matcher, + shouldUseScreen = false, + assertionType, }: Omit): RuleValidTestCase => { - const finalQuery = shouldUseScreen ? `screen.${query}` : query; - return { - code: `expect(${finalQuery}('Hello'))${matcher}`, - } as const; + const finalQuery = shouldUseScreen ? `screen.${query}` : query; + return { + code: `expect(${finalQuery}('Hello'))${matcher}`, + options: [ + { + [assertionType]: false, + [assertionType === 'absence' ? 'presence' : 'absence']: true, + }, + ], + }; }; -const getInvalidAssertion = ({ - query, - matcher, - messageId, - shouldUseScreen = false, -}: AssertionFnParams): RuleInvalidTestCase => { - const finalQuery = shouldUseScreen ? `screen.${query}` : query; - return { - code: `expect(${finalQuery}('Hello'))${matcher}`, - errors: [{ messageId, line: 1, column: shouldUseScreen ? 15 : 8 }], - }; +const toggleQueryPrefix = (query: string): string => { + if (query.startsWith('get')) return query.replace(/^get/, 'query'); + if (query.startsWith('query')) return query.replace(/^query/, 'get'); + return query; +}; + +const applyScreenPrefix = (query: string, shouldUseScreen: boolean): string => + shouldUseScreen ? `screen.${query}` : query; + +const getInvalidAssertions = ({ + query, + matcher, + messageId, + shouldUseScreen = false, + assertionType, +}: AssertionFnParams): RuleInvalidTestCase[] => { + const finalQuery = applyScreenPrefix(query, shouldUseScreen); + const code = `expect(${finalQuery}('Hello'))${matcher}`; + + const outputQuery = toggleQueryPrefix(query); + const finalOutputQuery = applyScreenPrefix(outputQuery, shouldUseScreen); + const output = `expect(${finalOutputQuery}('Hello'))${matcher}`; + + return [ + { + code, + errors: [{ messageId, line: 1, column: shouldUseScreen ? 15 : 8 }], + output, + }, + { + code, + options: [ + { + [assertionType]: true, + [assertionType === 'absence' ? 'presence' : 'absence']: false, + }, + ], + errors: [{ messageId, line: 1, column: shouldUseScreen ? 15 : 8 }], + output, + }, + ]; }; ruleTester.run(RULE_NAME, rule, { - valid: [ - // cases: methods not matching Testing Library queries pattern - `expect(queryElement('foo')).toBeInTheDocument()`, - `expect(getElement('foo')).not.toBeInTheDocument()`, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + valid: [ + // cases: methods not matching Testing Library queries pattern + `expect(queryElement('foo')).toBeInTheDocument()`, + `expect(getElement('foo')).not.toBeInTheDocument()`, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: invalid presence assert but not reported because custom module is not imported expect(queryByRole('button')).toBeInTheDocument() `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: invalid absence assert but not reported because custom module is not imported expect(getByRole('button')).not.toBeInTheDocument() `, - }, - // cases: asserting presence correctly with `getBy*` queries - ...getByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getValidAssertion({ - query: queryName, - matcher: '.toBeInTheDocument()', - }), - getValidAssertion({ query: queryName, matcher: '.toBeTruthy()' }), - getValidAssertion({ query: queryName, matcher: '.toBeDefined()' }), - getValidAssertion({ query: queryName, matcher: '.toBe("foo")' }), - getValidAssertion({ query: queryName, matcher: '.toEqual("World")' }), - getValidAssertion({ query: queryName, matcher: '.not.toBeFalsy()' }), - getValidAssertion({ query: queryName, matcher: '.not.toBeNull()' }), - getValidAssertion({ query: queryName, matcher: '.not.toBeDisabled()' }), - getValidAssertion({ - query: queryName, - matcher: '.not.toHaveClass("btn")', - }), - ], - [] - ), - // cases: asserting presence correctly with `screen.getBy*` queries - ...getByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getValidAssertion({ - query: queryName, - matcher: '.toBeInTheDocument()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toBeTruthy()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toBeDefined()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toBe("foo")', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toEqual("World")', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeFalsy()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeNull()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeDisabled()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toHaveClass("btn")', - shouldUseScreen: true, - }), - ], - [] - ), - // cases: asserting presence correctly with `getAllBy*` queries - ...getAllByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getValidAssertion({ - query: queryName, - matcher: '.toBeInTheDocument()', - }), - getValidAssertion({ query: queryName, matcher: '.toBeTruthy()' }), - getValidAssertion({ query: queryName, matcher: '.toBeDefined()' }), - getValidAssertion({ query: queryName, matcher: '.toBe("foo")' }), - getValidAssertion({ query: queryName, matcher: '.toEqual("World")' }), - getValidAssertion({ query: queryName, matcher: '.not.toBeFalsy()' }), - getValidAssertion({ query: queryName, matcher: '.not.toBeNull()' }), - getValidAssertion({ query: queryName, matcher: '.not.toBeDisabled()' }), - getValidAssertion({ - query: queryName, - matcher: '.not.toHaveClass("btn")', - }), - ], - [] - ), - // cases: asserting presence correctly with `screen.getAllBy*` queries - ...getAllByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getValidAssertion({ - query: queryName, - matcher: '.toBeInTheDocument()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toBeTruthy()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toBeDefined()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toBe("foo")', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toEqual("World")', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeFalsy()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeNull()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeDisabled()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toHaveClass("btn")', - shouldUseScreen: true, - }), - ], - [] - ), - // cases: asserting absence correctly with `queryBy*` queries - ...queryByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getValidAssertion({ query: queryName, matcher: '.toBeNull()' }), - getValidAssertion({ query: queryName, matcher: '.toBeFalsy()' }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeInTheDocument()', - }), - getValidAssertion({ query: queryName, matcher: '.not.toBeTruthy()' }), - getValidAssertion({ query: queryName, matcher: '.not.toBeDefined()' }), - getValidAssertion({ query: queryName, matcher: '.toEqual("World")' }), - getValidAssertion({ - query: queryName, - matcher: '.not.toHaveClass("btn")', - }), - ], - [] - ), - // cases: asserting absence correctly with `screen.queryBy*` queries - ...queryByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getValidAssertion({ - query: queryName, - matcher: '.toBeNull()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toBeFalsy()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeInTheDocument()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeTruthy()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeDefined()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toEqual("World")', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toHaveClass("btn")', - shouldUseScreen: true, - }), - ], - [] - ), - // cases: asserting absence correctly with `queryAllBy*` queries - ...queryAllByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getValidAssertion({ query: queryName, matcher: '.toBeNull()' }), - getValidAssertion({ query: queryName, matcher: '.toBeFalsy()' }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeInTheDocument()', - }), - getValidAssertion({ query: queryName, matcher: '.not.toBeTruthy()' }), - getValidAssertion({ query: queryName, matcher: '.not.toBeDefined()' }), - getValidAssertion({ query: queryName, matcher: '.toEqual("World")' }), - getValidAssertion({ - query: queryName, - matcher: '.not.toHaveClass("btn")', - }), - ], - [] - ), - // cases: asserting absence correctly with `screen.queryAllBy*` queries - ...queryAllByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getValidAssertion({ - query: queryName, - matcher: '.toBeNull()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toBeFalsy()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeInTheDocument()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeTruthy()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toBeDefined()', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.toEqual("World")', - shouldUseScreen: true, - }), - getValidAssertion({ - query: queryName, - matcher: '.not.toHaveClass("btn")', - shouldUseScreen: true, - }), - ], - [] - ), - { - code: 'const el = getByText("button")', - }, - { - code: 'const el = queryByText("button")', - }, - { - code: `async () => { + }, + // cases: asserting presence correctly with `getBy*` queries + ...getByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeInTheDocument()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeOnTheScreen()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeTruthy()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeDefined()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBe("foo")', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toEqual("World")', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeFalsy()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeNull()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeDisabled()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + assertionType: 'presence', + }), + ], + [] + ), + // cases: asserting presence correctly with `screen.getBy*` queries + ...getByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeInTheDocument()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeOnTheScreen()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeTruthy()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeDefined()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBe("foo")', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toEqual("World")', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeFalsy()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeNull()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeDisabled()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + shouldUseScreen: true, + assertionType: 'presence', + }), + ], + [] + ), + // cases: asserting presence correctly with `getAllBy*` queries + ...getAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeInTheDocument()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeOnTheScreen()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeTruthy()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeDefined()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBe("foo")', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toEqual("World")', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeFalsy()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeNull()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeDisabled()', + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + assertionType: 'presence', + }), + ], + [] + ), + // cases: asserting presence correctly with `screen.getAllBy*` queries + ...getAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeInTheDocument()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeOnTheScreen()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeTruthy()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeDefined()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBe("foo")', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toEqual("World")', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeFalsy()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeNull()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeDisabled()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + shouldUseScreen: true, + assertionType: 'presence', + }), + ], + [] + ), + // cases: asserting absence correctly with `queryBy*` queries + ...queryByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeNull()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeFalsy()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeOnTheScreen()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeTruthy()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeDefined()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toEqual("World")', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + assertionType: 'absence', + }), + ], + [] + ), + // cases: asserting absence correctly with `screen.queryBy*` queries + ...queryByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeNull()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeFalsy()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeOnTheScreen()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeTruthy()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeDefined()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toEqual("World")', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + shouldUseScreen: true, + assertionType: 'absence', + }), + ], + [] + ), + // cases: asserting absence correctly with `queryAllBy*` queries + ...queryAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeNull()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeFalsy()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeOnTheScreen()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeTruthy()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeDefined()', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toEqual("World")', + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + assertionType: 'absence', + }), + ], + [] + ), + // cases: asserting absence correctly with `screen.queryAllBy*` queries + ...queryAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeNull()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeFalsy()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeOnTheScreen()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeTruthy()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeDefined()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toEqual("World")', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + shouldUseScreen: true, + assertionType: 'absence', + }), + ], + [] + ), + + // cases: asserting absence incorrectly with `getBy*` queries with absence rule disabled + ...getByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeNull()', + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeFalsy()', + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument', + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeOnTheScreen', + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeTruthy()', + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeDefined()', + assertionType: 'absence', + }), + ], + [] + ), + // cases: asserting absence incorrectly with `screen.getBy*` queries with absence rule disabled + ...getByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeNull()', + shouldUseScreen: true, + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeFalsy()', + shouldUseScreen: true, + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument', + shouldUseScreen: true, + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeOnTheScreen', + shouldUseScreen: true, + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeTruthy()', + shouldUseScreen: true, + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeDefined()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ], + [] + ), + // cases: asserting absence incorrectly with `getAllBy*` queries with absence rule disabled + ...getAllByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeNull()', + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeFalsy()', + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument', + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeOnTheScreen', + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeTruthy()', + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeDefined()', + assertionType: 'absence', + }), + ], + [] + ), + // cases: asserting absence incorrectly with `screen.getAllBy*` queries with absence rule disabled + ...getAllByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeNull()', + shouldUseScreen: true, + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeFalsy()', + shouldUseScreen: true, + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument', + shouldUseScreen: true, + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeOnTheScreen', + shouldUseScreen: true, + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeTruthy()', + shouldUseScreen: true, + assertionType: 'absence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeDefined()', + shouldUseScreen: true, + assertionType: 'absence', + }), + ], + [] + ), + // cases: asserting presence incorrectly with `queryBy*` queries with presence rule disabled + ...queryByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeTruthy()', + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeDefined()', + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeOnTheScreen()', + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeFalsy()', + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeNull()', + assertionType: 'presence', + }), + ], + [] + ), + // cases: asserting presence incorrectly with `screen.queryBy*` queries with presence rule disabled + ...queryByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeTruthy()', + shouldUseScreen: true, + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeDefined()', + shouldUseScreen: true, + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + shouldUseScreen: true, + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeOnTheScreen()', + shouldUseScreen: true, + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeFalsy()', + shouldUseScreen: true, + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeNull()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ], + [] + ), + // cases: asserting presence incorrectly with `queryAllBy*` queries with presence rule disabled + ...queryAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeTruthy()', + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeDefined()', + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeOnTheScreen()', + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeFalsy()', + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeNull()', + assertionType: 'presence', + }), + ], + [] + ), + // cases: asserting presence incorrectly with `screen.queryAllBy*` queries with presence rule disabled + ...queryAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeTruthy()', + shouldUseScreen: true, + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeDefined()', + shouldUseScreen: true, + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + shouldUseScreen: true, + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.toBeOnTheScreen()', + shouldUseScreen: true, + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeFalsy()', + shouldUseScreen: true, + assertionType: 'presence', + }), + getDisabledValidAssertion({ + query: queryName, + matcher: '.not.toBeNull()', + shouldUseScreen: true, + assertionType: 'presence', + }), + ], + [] + ), + + { + code: 'const el = getByText("button")', + }, + { + code: 'const el = queryByText("button")', + }, + { + code: `async () => { const el = await findByText('button') expect(el).toBeInTheDocument() }`, - }, - `// case: query an element with getBy but then check its absence after doing + }, + `// case: query an element with getBy but then check its absence after doing // some action which makes it disappear. // submit button exists const submitButton = screen.getByRole('button') fireEvent.click(submitButton) - + // right after clicking submit button it disappears expect(submitButton).not.toBeInTheDocument() `, - ], - invalid: [ - // cases: asserting absence incorrectly with `getBy*` queries - ...getByQueries.reduce( - (invalidRules, queryName) => [ - ...invalidRules, - getInvalidAssertion({ - query: queryName, - matcher: '.toBeNull()', - messageId: 'wrongAbsenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeFalsy()', - messageId: 'wrongAbsenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeInTheDocument()', - messageId: 'wrongAbsenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeTruthy()', - messageId: 'wrongAbsenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeDefined()', - messageId: 'wrongAbsenceQuery', - }), - ], - [] - ), - // cases: asserting absence incorrectly with `screen.getBy*` queries - ...getByQueries.reduce( - (invalidRules, queryName) => [ - ...invalidRules, - getInvalidAssertion({ - query: queryName, - matcher: '.toBeNull()', - messageId: 'wrongAbsenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeFalsy()', - messageId: 'wrongAbsenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeInTheDocument()', - messageId: 'wrongAbsenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeTruthy()', - messageId: 'wrongAbsenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeDefined()', - messageId: 'wrongAbsenceQuery', - shouldUseScreen: true, - }), - ], - [] - ), - // cases: asserting absence incorrectly with `getAllBy*` queries - ...getAllByQueries.reduce( - (invalidRules, queryName) => [ - ...invalidRules, - getInvalidAssertion({ - query: queryName, - matcher: '.toBeNull()', - messageId: 'wrongAbsenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeFalsy()', - messageId: 'wrongAbsenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeInTheDocument()', - messageId: 'wrongAbsenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeTruthy()', - messageId: 'wrongAbsenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeDefined()', - messageId: 'wrongAbsenceQuery', - }), - ], - [] - ), - // cases: asserting absence incorrectly with `screen.getAllBy*` queries - ...getAllByQueries.reduce( - (invalidRules, queryName) => [ - ...invalidRules, - getInvalidAssertion({ - query: queryName, - matcher: '.toBeNull()', - messageId: 'wrongAbsenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeFalsy()', - messageId: 'wrongAbsenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeInTheDocument()', - messageId: 'wrongAbsenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeTruthy()', - messageId: 'wrongAbsenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeDefined()', - messageId: 'wrongAbsenceQuery', - shouldUseScreen: true, - }), - ], - [] - ), - // cases: asserting presence incorrectly with `queryBy*` queries - ...queryByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getInvalidAssertion({ - query: queryName, - matcher: '.toBeTruthy()', - messageId: 'wrongPresenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeDefined()', - messageId: 'wrongPresenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeInTheDocument()', - messageId: 'wrongPresenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeFalsy()', - messageId: 'wrongPresenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeNull()', - messageId: 'wrongPresenceQuery', - }), - ], - [] - ), - // cases: asserting presence incorrectly with `screen.queryBy*` queries - ...queryByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getInvalidAssertion({ - query: queryName, - matcher: '.toBeTruthy()', - messageId: 'wrongPresenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeDefined()', - messageId: 'wrongPresenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeInTheDocument()', - messageId: 'wrongPresenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeFalsy()', - messageId: 'wrongPresenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeNull()', - messageId: 'wrongPresenceQuery', - shouldUseScreen: true, - }), - ], - [] - ), - // cases: asserting presence incorrectly with `queryAllBy*` queries - ...queryAllByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getInvalidAssertion({ - query: queryName, - matcher: '.toBeTruthy()', - messageId: 'wrongPresenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeDefined()', - messageId: 'wrongPresenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeInTheDocument()', - messageId: 'wrongPresenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeFalsy()', - messageId: 'wrongPresenceQuery', - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeNull()', - messageId: 'wrongPresenceQuery', - }), - ], - [] - ), - // cases: asserting presence incorrectly with `screen.queryAllBy*` queries - ...queryAllByQueries.reduce( - (validRules, queryName) => [ - ...validRules, - getInvalidAssertion({ - query: queryName, - matcher: '.toBeTruthy()', - messageId: 'wrongPresenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeDefined()', - messageId: 'wrongPresenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.toBeInTheDocument()', - messageId: 'wrongPresenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeFalsy()', - messageId: 'wrongPresenceQuery', - shouldUseScreen: true, - }), - getInvalidAssertion({ - query: queryName, - matcher: '.not.toBeNull()', - messageId: 'wrongPresenceQuery', - shouldUseScreen: true, - }), - ], - [] - ), - { - code: 'expect(screen.getAllByText("button")[1]).not.toBeInTheDocument()', - errors: [{ messageId: 'wrongAbsenceQuery', line: 1, column: 15 }], - }, - { - code: 'expect(screen.queryAllByText("button")[1]).toBeInTheDocument()', - errors: [{ messageId: 'wrongPresenceQuery', line: 1, column: 15 }], - }, - { - code: ` + `// checking absence on getBy* inside a within with queryBy* outside the within + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeInTheDocument() + `, + `// checking presence on getBy* inside a within with getBy* outside the within + expect(within(screen.getByRole("button")).getByText("Hello")).toBeInTheDocument() + `, + ], + invalid: [ + // cases: asserting absence incorrectly with `getBy*` queries + ...getByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeNull()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeFalsy()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeOnTheScreen()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeTruthy()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeDefined()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ], + [] + ), + // cases: asserting absence incorrectly with `screen.getBy*` queries + ...getByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeNull()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeFalsy()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeOnTheScreen()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeTruthy()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeDefined()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ], + [] + ), + // cases: asserting absence incorrectly with `getAllBy*` queries + ...getAllByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeNull()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeFalsy()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeOnTheScreen()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeTruthy()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeDefined()', + messageId: 'wrongAbsenceQuery', + assertionType: 'absence', + }), + ], + [] + ), + // cases: asserting absence incorrectly with `screen.getAllBy*` queries + ...getAllByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeNull()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeFalsy()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeOnTheScreen()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeTruthy()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeDefined()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + assertionType: 'absence', + }), + ], + [] + ), + // cases: asserting presence incorrectly with `queryBy*` queries + ...queryByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeTruthy()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeDefined()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeInTheDocument()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeOnTheScreen()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeFalsy()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeNull()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ], + [] + ), + // cases: asserting presence incorrectly with `screen.queryBy*` queries + ...queryByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeTruthy()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeDefined()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeInTheDocument()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeOnTheScreen()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeFalsy()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeNull()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ], + [] + ), + // cases: asserting presence incorrectly with `queryAllBy*` queries + ...queryAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeTruthy()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeDefined()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeInTheDocument()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeOnTheScreen()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeFalsy()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeNull()', + messageId: 'wrongPresenceQuery', + assertionType: 'presence', + }), + ], + [] + ), + // cases: asserting presence incorrectly with `screen.queryAllBy*` queries + ...queryAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeTruthy()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeDefined()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeInTheDocument()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeOnTheScreen()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeFalsy()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ...getInvalidAssertions({ + query: queryName, + matcher: '.not.toBeNull()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + assertionType: 'presence', + }), + ], + [] + ), + { + code: 'expect(screen.getAllByText("button")[1]).not.toBeInTheDocument()', + errors: [{ messageId: 'wrongAbsenceQuery', line: 1, column: 15 }], + output: + 'expect(screen.queryAllByText("button")[1]).not.toBeInTheDocument()', + }, + { + code: 'expect(screen.getAllByText("button")[1]).not.toBeOnTheScreen()', + errors: [{ messageId: 'wrongAbsenceQuery', line: 1, column: 15 }], + output: + 'expect(screen.queryAllByText("button")[1]).not.toBeOnTheScreen()', + }, + { + code: 'expect(screen.queryAllByText("button")[1]).toBeInTheDocument()', + errors: [{ messageId: 'wrongPresenceQuery', line: 1, column: 15 }], + output: 'expect(screen.getAllByText("button")[1]).toBeInTheDocument()', + }, + { + code: 'expect(screen.queryAllByText("button")[1]).toBeOnTheScreen()', + errors: [{ messageId: 'wrongPresenceQuery', line: 1, column: 15 }], + output: 'expect(screen.getAllByText("button")[1]).toBeOnTheScreen()', + }, + { + code: ` // case: asserting presence incorrectly with custom queryBy* query expect(queryByCustomQuery("button")).toBeInTheDocument() `, - errors: [{ messageId: 'wrongPresenceQuery', line: 3, column: 16 }], - }, - { - code: ` + errors: [{ messageId: 'wrongPresenceQuery', line: 3, column: 16 }], + output: ` + // case: asserting presence incorrectly with custom queryBy* query + expect(getByCustomQuery("button")).toBeInTheDocument() + `, + }, + { + code: ` + // case: asserting presence incorrectly with custom queryBy* query + expect(queryByCustomQuery("button")).toBeOnTheScreen() + `, + errors: [{ messageId: 'wrongPresenceQuery', line: 3, column: 16 }], + output: ` + // case: asserting presence incorrectly with custom queryBy* query + expect(getByCustomQuery("button")).toBeOnTheScreen() + `, + }, + { + code: ` // case: asserting absence incorrectly with custom getBy* query expect(getByCustomQuery("button")).not.toBeInTheDocument() `, - errors: [{ messageId: 'wrongAbsenceQuery', line: 3, column: 16 }], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ messageId: 'wrongAbsenceQuery', line: 3, column: 16 }], + output: ` + // case: asserting absence incorrectly with custom getBy* query + expect(queryByCustomQuery("button")).not.toBeInTheDocument() + `, + }, + { + code: ` + // case: asserting absence incorrectly with custom getBy* query + expect(getByCustomQuery("button")).not.toBeOnTheScreen() + `, + errors: [{ messageId: 'wrongAbsenceQuery', line: 3, column: 16 }], + output: ` + // case: asserting absence incorrectly with custom getBy* query + expect(queryByCustomQuery("button")).not.toBeOnTheScreen() + `, + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: asserting presence incorrectly importing custom module import 'test-utils' expect(queryByRole("button")).toBeInTheDocument() `, - errors: [{ line: 4, column: 14, messageId: 'wrongPresenceQuery' }], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ line: 4, column: 14, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting presence incorrectly importing custom module + import 'test-utils' + expect(getByRole("button")).toBeInTheDocument() + `, + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case: asserting presence incorrectly importing custom module + import 'test-utils' + expect(queryByRole("button")).toBeOnTheScreen() + `, + errors: [{ line: 4, column: 14, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting presence incorrectly importing custom module + import 'test-utils' + expect(getByRole("button")).toBeOnTheScreen() + `, + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // case: asserting absence incorrectly importing custom module import 'test-utils' expect(getByRole("button")).not.toBeInTheDocument() `, - errors: [{ line: 4, column: 14, messageId: 'wrongAbsenceQuery' }], - }, - ], + errors: [{ line: 4, column: 14, messageId: 'wrongAbsenceQuery' }], + output: ` + // case: asserting absence incorrectly importing custom module + import 'test-utils' + expect(queryByRole("button")).not.toBeInTheDocument() + `, + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case: asserting absence incorrectly importing custom module + import 'test-utils' + expect(getByRole("button")).not.toBeOnTheScreen() + `, + errors: [{ line: 4, column: 14, messageId: 'wrongAbsenceQuery' }], + output: ` + // case: asserting absence incorrectly importing custom module + import 'test-utils' + expect(queryByRole("button")).not.toBeOnTheScreen() + `, + }, + { + code: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).getByText("Hello")).not.toBeInTheDocument()`, + errors: [{ line: 3, column: 46, messageId: 'wrongAbsenceQuery' }], + output: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`, + }, + { + code: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).queryByText("Hello")).toBeInTheDocument()`, + errors: [{ line: 3, column: 46, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeInTheDocument()`, + }, + { + code: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.queryByRole("button")).getByText("Hello")).not.toBeInTheDocument()`, + errors: [ + { line: 3, column: 25, messageId: 'wrongPresenceQuery' }, + { line: 3, column: 48, messageId: 'wrongAbsenceQuery' }, + ], + output: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`, + }, + { + code: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.queryByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`, + errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`, + }, + { + code: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.queryByRole("button")).getByText("Hello")).toBeInTheDocument()`, + errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeInTheDocument()`, + }, + { + code: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.queryByRole("button")).queryByText("Hello")).toBeInTheDocument()`, + errors: [ + { line: 3, column: 25, messageId: 'wrongPresenceQuery' }, + { line: 3, column: 48, messageId: 'wrongPresenceQuery' }, + ], + output: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeInTheDocument()`, + }, + { + code: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).getByText("Hello")).not.toBeOnTheScreen()`, + errors: [{ line: 3, column: 46, messageId: 'wrongAbsenceQuery' }], + output: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`, + }, + { + code: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).queryByText("Hello")).toBeOnTheScreen()`, + errors: [{ line: 3, column: 46, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeOnTheScreen()`, + }, + { + code: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.queryByRole("button")).getByText("Hello")).not.toBeOnTheScreen()`, + errors: [ + { line: 3, column: 25, messageId: 'wrongPresenceQuery' }, + { line: 3, column: 48, messageId: 'wrongAbsenceQuery' }, + ], + output: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`, + }, + { + code: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.queryByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`, + errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`, + }, + { + code: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.queryByRole("button")).getByText("Hello")).toBeOnTheScreen()`, + errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeOnTheScreen()`, + }, + { + code: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.queryByRole("button")).queryByText("Hello")).toBeOnTheScreen()`, + errors: [ + { line: 3, column: 25, messageId: 'wrongPresenceQuery' }, + { line: 3, column: 48, messageId: 'wrongPresenceQuery' }, + ], + output: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeOnTheScreen()`, + }, + ], }); diff --git a/tests/lib/rules/prefer-query-by-disappearance.test.ts b/tests/lib/rules/prefer-query-by-disappearance.test.ts index 3f9d5460..32eb2f3b 100644 --- a/tests/lib/rules/prefer-query-by-disappearance.test.ts +++ b/tests/lib/rules/prefer-query-by-disappearance.test.ts @@ -1,757 +1,765 @@ import rule, { - RULE_NAME, + RULE_NAME, } from '../../../lib/rules/prefer-query-by-disappearance'; import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: ` - import { screen } from '@testing-library/react'; + valid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { screen } from '${testingFramework}'; const button = screen.getByRole('button') await waitForElementToBeRemoved(button) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; const callback = () => screen.getByRole('button') await waitForElementToBeRemoved(callback) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(() => screen.queryByText("hello")) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(() => { screen.queryByText("hello") }) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(() => { otherCode() screen.queryByText("hello") }) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(() => { otherCode() return screen.queryByText("hello") }) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(() => { return screen.queryByText("hello") }) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(function() { screen.queryByText("hello") }) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(function() { otherCode() screen.queryByText("hello") }) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(function() { return screen.queryByText('hey') }) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(function() { otherCode() return screen.queryByText('hey') }) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(screen.queryByText("hello")) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; const { queryByText } = screen await waitForElementToBeRemoved(queryByText("hello")) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; const { queryByText } = screen await waitForElementToBeRemoved(() => queryByText("hello")) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; const { queryByText } = screen await waitForElementToBeRemoved(() => { queryByText("hello") }) `, - }, - { - code: ` - import { screen } from '@testing-library/react'; + }, + { + code: ` + import { screen } from '${testingFramework}'; const { queryByText } = screen await waitForElementToBeRemoved(() => { return queryByText("hello") }) `, - }, - { - code: ` - import { render } from '@testing-library/react'; + }, + { + code: ` + import { render } from '${testingFramework}'; const { queryByText } = render() await waitForElementToBeRemoved(() => { return queryByText("hello") }) `, - }, - { - code: ` - import { render } from '@testing-library/react'; + }, + { + code: ` + import { render } from '${testingFramework}'; const { queryByText } = render() await waitForElementToBeRemoved(() => { queryByText("hello") }) `, - }, - { - code: ` - import { render } from '@testing-library/react'; + }, + { + code: ` + import { render } from '${testingFramework}'; const { queryByText } = render() await waitForElementToBeRemoved(() => queryByText("hello")) `, - }, - { - code: ` - import { render } from '@testing-library/react'; + }, + { + code: ` + import { render } from '${testingFramework}'; const { queryByText } = render() await waitForElementToBeRemoved(queryByText("hello")) `, - }, - { - code: ` - import { render } from '@testing-library/react'; + }, + { + code: ` + import { render } from '${testingFramework}'; const { queryByText } = render() await waitForElementToBeRemoved(function() { queryByText("hello") }) `, - }, - { - code: ` - import { render } from '@testing-library/react'; + }, + { + code: ` + import { render } from '${testingFramework}'; const { queryByText } = render() await waitForElementToBeRemoved(function() { return queryByText("hello") }) `, - }, - ], - invalid: [ - { - code: ` - import { screen, waitForElementToBeRemoved } from '@testing-library/react'; + }, + ]), + invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { screen, waitForElementToBeRemoved } from '${testingFramework}'; await waitForElementToBeRemoved(() => screen.getByText("hello")) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen, waitForElementToBeRemoved } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 4, + column: 54, + }, + ], + }, + { + code: ` + import { screen, waitForElementToBeRemoved } from '${testingFramework}'; await waitForElementToBeRemoved(() => screen.findByText("hello")) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen, waitForElementToBeRemoved } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 4, + column: 54, + }, + ], + }, + { + code: ` + import { screen, waitForElementToBeRemoved } from '${testingFramework}'; await waitForElementToBeRemoved(() => { screen.getByText("hello") }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen, waitForElementToBeRemoved } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 18, + }, + ], + }, + { + code: ` + import { screen, waitForElementToBeRemoved } from '${testingFramework}'; await waitForElementToBeRemoved(() => { screen.findByText("hello") }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen, waitForElementToBeRemoved } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 18, + }, + ], + }, + { + code: ` + import { screen, waitForElementToBeRemoved } from '${testingFramework}'; await waitForElementToBeRemoved(() => { return screen.getByText("hello") }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen, waitForElementToBeRemoved } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 25, + }, + ], + }, + { + code: ` + import { screen, waitForElementToBeRemoved } from '${testingFramework}'; await waitForElementToBeRemoved(() => { return screen.findByText("hello") }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen, waitForElementToBeRemoved } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 25, + }, + ], + }, + { + code: ` + import { screen, waitForElementToBeRemoved } from '${testingFramework}'; await waitForElementToBeRemoved(screen.getByText("hello")) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen, waitForElementToBeRemoved } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 4, + column: 48, + }, + ], + }, + { + code: ` + import { screen, waitForElementToBeRemoved } from '${testingFramework}'; await waitForElementToBeRemoved(screen.findByText("hello")) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 4, + column: 48, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(function() { return screen.getByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 25, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(function() { return screen.findByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 25, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(function() { screen.getByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 18, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; await waitForElementToBeRemoved(function() { screen.findByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 4, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 18, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { getByText } = screen await waitForElementToBeRemoved(function() { getByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 11, + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; const { getByText } = render() await waitForElementToBeRemoved(function() { getByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 11, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { findByText } = screen await waitForElementToBeRemoved(function() { findByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 11, + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; const { findByText } = render await waitForElementToBeRemoved(function() { findByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 11, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { getByText } = screen await waitForElementToBeRemoved(function() { return getByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; const { getByText } = render() await waitForElementToBeRemoved(function() { return getByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { findByText } = screen await waitForElementToBeRemoved(function() { return findByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; const { findByText } = render() await waitForElementToBeRemoved(function() { return findByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { getByText } = screen await waitForElementToBeRemoved(() => { return getByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; const { getByText } = render() await waitForElementToBeRemoved(() => { return getByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { findByText } = screen await waitForElementToBeRemoved(() => { return findByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { findByText } = render() await waitForElementToBeRemoved(() => { return findByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { getByText } = screen await waitForElementToBeRemoved(() => { getByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 11, + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; const { getByText } = render() await waitForElementToBeRemoved(() => { getByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 11, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { findByText } = screen await waitForElementToBeRemoved(() => { findByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 11, + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; const { findByText } = render() await waitForElementToBeRemoved(() => { findByText('hey') }) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 6, + column: 11, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { findByText } = screen await waitForElementToBeRemoved(() => findByText('hey')) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 47, + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; const { findByText } = render() await waitForElementToBeRemoved(() => findByText('hey')) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 47, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { getByText } = screen await waitForElementToBeRemoved(getByText('hey')) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 41, + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; const { getByText } = render() await waitForElementToBeRemoved(getByText('hey')) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { screen } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 41, + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; const { findByText } = screen await waitForElementToBeRemoved(findByText('hey')) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 41, + }, + ], + }, + { + code: ` + import { render } from '${testingFramework}'; const { findByText } = render() await waitForElementToBeRemoved(findByText('hey')) `, - errors: [ - { - messageId: 'preferQueryByDisappearance', - line: 5, - column: 41, - }, - ], - }, - ], + errors: [ + { + messageId: 'preferQueryByDisappearance', + line: 5, + column: 41, + }, + ], + }, + ]), }); diff --git a/tests/lib/rules/prefer-query-matchers.test.ts b/tests/lib/rules/prefer-query-matchers.test.ts new file mode 100644 index 00000000..66f03644 --- /dev/null +++ b/tests/lib/rules/prefer-query-matchers.test.ts @@ -0,0 +1,417 @@ +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; + +import rule, { + RULE_NAME, + MessageIds, + Options, +} from '../../../lib/rules/prefer-query-matchers'; +import { ALL_QUERIES_METHODS } from '../../../lib/utils'; +import { createRuleTester } from '../test-utils'; + +const ruleTester = createRuleTester(); + +const getByQueries = ALL_QUERIES_METHODS.map((method) => `get${method}`); +const getAllByQueries = ALL_QUERIES_METHODS.map((method) => `getAll${method}`); +const queryByQueries = ALL_QUERIES_METHODS.map((method) => `query${method}`); +const queryAllByQueries = ALL_QUERIES_METHODS.map( + (method) => `queryAll${method}` +); + +type RuleValidTestCase = ValidTestCase; +type RuleInvalidTestCase = InvalidTestCase; + +type AssertionFnParams = { + query: string; + matcher: string; + options: Options; + onlyOptions?: boolean; +}; + +const wrapExpectInTest = (expectStatement: string) => ` +import { render, screen } from '@testing-library/react' + +test('a fake test', () => { + render() + + ${expectStatement} +})`; + +const getValidAssertions = ({ + query, + matcher, + options, + onlyOptions = false, +}: AssertionFnParams): RuleValidTestCase[] => { + const expectStatement = `expect(${query}('Hello'))${matcher}`; + const expectScreenStatement = `expect(screen.${query}('Hello'))${matcher}`; + const casesWithOptions = [ + { + // name: `${expectStatement} with provided options`, + code: wrapExpectInTest(expectStatement), + options, + }, + { + // name: `${expectScreenStatement} with provided options`, + code: wrapExpectInTest(expectScreenStatement), + options, + }, + ]; + + if (onlyOptions) { + return casesWithOptions; + } + + return [ + { + // name: `${expectStatement} with default options of empty validEntries`, + code: wrapExpectInTest(expectStatement), + }, + { + // name: `${expectScreenStatement} with default options of empty validEntries`, + code: wrapExpectInTest(expectScreenStatement), + }, + ...casesWithOptions, + ]; +}; + +const getInvalidAssertions = ({ + query, + matcher, + options, +}: AssertionFnParams): RuleInvalidTestCase[] => { + const expectStatement = `expect(${query}('Hello'))${matcher}`; + const expectScreenStatement = `expect(screen.${query}('Hello'))${matcher}`; + const messageId: MessageIds = 'wrongQueryForMatcher'; + const [ + { + validEntries: [validEntry], + }, + ] = options; + return [ + { + // name: `${expectStatement} with provided options`, + code: wrapExpectInTest(expectStatement), + options, + errors: [ + { + messageId, + line: 7, + column: 10, + data: { query: validEntry.query, matcher: validEntry.matcher }, + }, + ], + }, + { + // name: `${expectScreenStatement} with provided options`, + code: wrapExpectInTest(expectScreenStatement), + options, + errors: [ + { + messageId, + line: 7, + column: 17, + data: { query: validEntry.query, matcher: validEntry.matcher }, + }, + ], + }, + ]; +}; + +ruleTester.run(RULE_NAME, rule, { + valid: [ + // cases: methods not matching Testing Library queries pattern + `expect(queryElement('foo')).toBeVisible()`, + `expect(getElement('foo')).not.toBeVisible()`, + // cases: asserting with a configured allowed `[screen.]getBy*` query + ...getByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeVisible()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeVisible()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeVisible' }] }, + ], + onlyOptions: true, + }), + ], + [] + ), + // cases: asserting with a configured allowed `[screen.]getAllBy*` query + ...getAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeVisible()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeVisible()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeVisible' }] }, + ], + onlyOptions: true, + }), + ], + [] + ), + // cases: asserting with a configured allowed `[screen.]queryBy*` query + ...queryByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeVisible()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeVisible()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + onlyOptions: true, + }), + ], + [] + ), + // cases: asserting with a configured allowed `[screen.]queryAllBy*` query + ...queryAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getValidAssertions({ + query: queryName, + matcher: '.toBeVisible()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeVisible()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.not.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeVisible' }] }, + ], + }), + ...getValidAssertions({ + query: queryName, + matcher: '.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + onlyOptions: true, + }), + ], + [] + ), + // case: getting outside an expectation + { + code: 'const el = getByText("button")', + }, + // case: querying outside an expectation + { + code: 'const el = queryByText("button")', + }, + // case: finding outside an expectation + { + code: `async () => { + const el = await findByText('button') + expect(el).toBeVisible() + }`, + }, + { + code: `// case: query an element with getBy but then check its absence after doing + // some action which makes it disappear. + + // submit button exists + const submitButton = screen.getByRole('button') + fireEvent.click(submitButton) + + // right after clicking submit button it disappears + expect(submitButton).toBeHelloWorld() + `, + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeHelloWorld' }] }, + ], + }, + ], + invalid: [ + // cases: asserting with a disallowed `[screen.]getBy*` query + ...getByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeHelloWorld' }] }, + ], + }), + ], + [] + ), + // cases: asserting with a disallowed `[screen.]getAllBy*` query + ...getAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeHelloWorld()', + options: [ + { validEntries: [{ query: 'query', matcher: 'toBeHelloWorld' }] }, + ], + }), + ], + [] + ), + // cases: asserting with a disallowed `[screen.]getBy*` query + ...queryByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeVisible()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + }), + ], + [] + ), + // cases: asserting with a disallowed `[screen.]queryAllBy*` query + ...queryAllByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + ...getInvalidAssertions({ + query: queryName, + matcher: '.toBeVisible()', + options: [ + { validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }, + ], + }), + ], + [] + ), + // cases: indexing into an `AllBy` result within the expectation + { + code: 'expect(screen.queryAllByText("button")[1]).toBeVisible()', + options: [{ validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }], + errors: [ + { + messageId: 'wrongQueryForMatcher', + line: 1, + column: 15, + data: { query: 'get', matcher: 'toBeVisible' }, + }, + ], + }, + { + code: ` + // case: asserting presence incorrectly with custom queryBy* query + expect(queryByCustomQuery("button")).toBeVisible() + `, + options: [{ validEntries: [{ query: 'get', matcher: 'toBeVisible' }] }], + errors: [ + { + messageId: 'wrongQueryForMatcher', + line: 3, + column: 12, + data: { query: 'get', matcher: 'toBeVisible' }, + }, + ], + }, + ], +}); diff --git a/tests/lib/rules/prefer-screen-queries.test.ts b/tests/lib/rules/prefer-screen-queries.test.ts index c94a1474..b49ae2ad 100644 --- a/tests/lib/rules/prefer-screen-queries.test.ts +++ b/tests/lib/rules/prefer-screen-queries.test.ts @@ -1,492 +1,486 @@ import rule, { RULE_NAME } from '../../../lib/rules/prefer-screen-queries'; import { - ALL_QUERIES_COMBINATIONS, - ALL_QUERIES_VARIANTS, - combineQueries, + ALL_QUERIES_COMBINATIONS, + ALL_QUERIES_VARIANTS, + combineQueries, } from '../../../lib/utils'; import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + const CUSTOM_QUERY_COMBINATIONS = combineQueries(ALL_QUERIES_VARIANTS, [ - 'ByIcon', + 'ByIcon', ]); ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: `const baz = () => 'foo'`, - }, - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: `screen.${queryMethod}()`, - })), - { - code: `otherFunctionShouldNotThrow()`, - }, - { - code: `component.otherFunctionShouldNotThrow()`, - }, - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: `within(component).${queryMethod}()`, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: `within(screen.${queryMethod}()).${queryMethod}()`, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` + valid: [ + { + code: `const baz = () => 'foo'`, + }, + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: `screen.${queryMethod}()`, + })), + { + code: `otherFunctionShouldNotThrow()`, + }, + { + code: `component.otherFunctionShouldNotThrow()`, + }, + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: `within(component).${queryMethod}()`, + })), + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: `within(screen.${queryMethod}()).${queryMethod}()`, + })), + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` const { ${queryMethod} } = within(screen.getByText('foo')) ${queryMethod}(baz) `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` + })), + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` const myWithinVariable = within(foo) myWithinVariable.${queryMethod}('baz') `, - })), - ...CUSTOM_QUERY_COMBINATIONS.map( - (query) => ` - import { render } from '@testing-library/react' - import { ${query} } from 'custom-queries' - - test("imported custom queries, since they can't be used through screen", () => { - render(foo) - ${query}('bar') - }) - ` - ), - ...CUSTOM_QUERY_COMBINATIONS.map( - (query) => ` - import { render } from '@testing-library/react' - - test("render-returned custom queries, since they can't be used through screen", () => { - const { ${query} } = render(foo) - ${query}('bar') - }) - ` - ), - ...CUSTOM_QUERY_COMBINATIONS.map((query) => ({ - settings: { - 'testing-library/custom-queries': [query, 'ByComplexText'], - }, - code: ` - import { render } from '@testing-library/react' - - test("custom queries + custom-queries setting, since they can't be used through screen", () => { - const { ${query} } = render(foo) - ${query}('bar') - }) - `, - })), - { - code: ` + })), + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + ...CUSTOM_QUERY_COMBINATIONS.map((query) => ({ + code: ` + import { render } from '${testingFramework}' + import { ${query} } from 'custom-queries' + + test("imported custom queries, since they can't be used through screen", () => { + render(foo) + ${query}('bar') + }) + `, + })), + ...CUSTOM_QUERY_COMBINATIONS.map((query) => ({ + code: ` + import { render } from '${testingFramework}' + + test("render-returned custom queries, since they can't be used through screen", () => { + const { ${query} } = render(foo) + ${query}('bar') + }) + `, + })), + ...CUSTOM_QUERY_COMBINATIONS.map((query) => ({ + settings: { + 'testing-library/custom-queries': [query, 'ByComplexText'], + }, + code: ` + import { render } from '${testingFramework}' + + test("custom queries + custom-queries setting, since they can't be used through screen", () => { + const { ${query} } = render(foo) + ${query}('bar') + }) + `, + })), + ]), + { + code: ` const screen = render(baz); screen.container.querySelector('foo'); `, - }, - { - code: ` + }, + { + code: ` const screen = render(baz); screen.baseElement.querySelector('foo'); `, - }, - { - code: ` + }, + { + code: ` const { rerender } = render(baz); rerender(); `, - }, - { - code: ` + }, + { + code: ` const utils = render(baz); utils.rerender(); `, - }, - { - code: ` + }, + { + code: ` const utils = render(baz); utils.asFragment(); `, - }, - { - code: ` + }, + { + code: ` const { asFragment } = render(baz); asFragment(); `, - }, - { - code: ` + }, + { + code: ` const { unmount } = render(baz); unmount(); `, - }, - { - code: ` + }, + { + code: ` const utils = render(baz); utils.unmount(); `, - }, - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + }, + ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ + code: ` const { ${queryMethod} } = render(baz, { baseElement: treeA }) expect(${queryMethod}(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + })), + ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ + code: ` const { ${queryMethod}: aliasMethod } = render(baz, { baseElement: treeA }) expect(aliasMethod(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + })), + ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ + code: ` const { ${queryMethod} } = render(baz, { container: treeA }) expect(${queryMethod}(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + })), + ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ + code: ` const { ${queryMethod}: aliasMethod } = render(baz, { container: treeA }) expect(aliasMethod(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + })), + ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ + code: ` const { ${queryMethod} } = render(baz, { baseElement: treeB, container: treeA }) expect(${queryMethod}(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + })), + ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ + code: ` const { ${queryMethod}: aliasMethod } = render(baz, { baseElement: treeB, container: treeA }) expect(aliasMethod(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + })), + ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ + code: ` render(foo, { baseElement: treeA }).${queryMethod}() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + })), + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { render as testUtilRender } from 'test-utils' import { render } from 'somewhere-else' const { ${queryMethod} } = render(foo) ${queryMethod}()`, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ - settings: { - 'testing-library/custom-renders': ['customRender'], - }, - code: ` + })), + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + settings: { + 'testing-library/custom-renders': ['customRender'], + }, + code: ` import { anotherRender } from 'whatever' const { ${queryMethod} } = anotherRender(foo) ${queryMethod}()`, - })), - ], + })), + ], - invalid: [ - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - code: ` + invalid: [ + ...ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const { ${queryMethod} } = render(foo) ${queryMethod}()`, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ), + ...ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { render } from 'test-utils' const { ${queryMethod} } = render(foo) ${queryMethod}()`, - errors: [ - { - line: 4, - column: 9, - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), + errors: [ + { + line: 4, + column: 9, + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - settings: { - 'testing-library/custom-renders': ['customRender'], - }, - code: ` + ...ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + settings: { + 'testing-library/custom-renders': ['customRender'], + }, + code: ` import { customRender } from 'whatever' const { ${queryMethod} } = customRender(foo) ${queryMethod}()`, - errors: [ - { - line: 4, - column: 9, - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render as testingLibraryRender} from '@testing-library/react' + errors: [ + { + line: 4, + column: 9, + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ), + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => + ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingLibraryRender} from '${testingFramework}' const { ${queryMethod} } = testingLibraryRender(foo) ${queryMethod}()`, - errors: [ - { - line: 4, - column: 9, - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from 'test-utils' - const { ${queryMethod} } = render(foo) - ${queryMethod}()`, - errors: [ - { - line: 4, - column: 9, - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - code: `render().${queryMethod}()`, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - code: `render(foo, { hydrate: true }).${queryMethod}()`, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - code: `component.${queryMethod}()`, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - code: ` + errors: [ + { + line: 4, + column: 9, + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ) + ), + ...ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: `render().${queryMethod}()`, + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ), + ...ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: `render(foo, { hydrate: true }).${queryMethod}()`, + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ), + ...ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: `component.${queryMethod}()`, + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ), + ...ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const { ${queryMethod} } = render() ${queryMethod}(baz) `, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - code: ` + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ), + ...ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const myRenderVariable = render() myRenderVariable.${queryMethod}(baz) `, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - code: ` + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ), + ...ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const [myVariable] = render() myVariable.${queryMethod}(baz) `, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - code: ` + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ), + ...ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const { ${queryMethod} } = render(baz, { hydrate: true }) ${queryMethod}(baz) `, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - ...ALL_QUERIES_COMBINATIONS.map( - (queryMethod) => - ({ - code: ` + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ), + ...ALL_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const [myVariable] = within() myVariable.${queryMethod}(baz) `, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - } as const) - ), - { - code: ` // issue #367 - example A - import { render } from '@testing-library/react'; - + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + }) as const + ), + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` // issue #367 - example A + import { render } from '${testingFramework}'; + function setup() { return render(
); } - + it('foo', async () => { const { getByText } = await setup(); expect(getByText('foo')).toBeInTheDocument(); }); - + it('bar', () => { const { getByText } = setup(); expect(getByText('foo')).toBeInTheDocument(); }); `, - errors: [ - { - messageId: 'preferScreenQueries', - line: 10, - column: 16, - data: { - name: 'getByText', - }, - }, - { - messageId: 'preferScreenQueries', - line: 15, - column: 16, - data: { - name: 'getByText', - }, - }, - ], - }, - { - code: ` // issue #367 - example B - import { render } from '@testing-library/react'; - + errors: [ + { + messageId: 'preferScreenQueries', + line: 10, + column: 16, + data: { + name: 'getByText', + }, + }, + { + messageId: 'preferScreenQueries', + line: 15, + column: 16, + data: { + name: 'getByText', + }, + }, + ], + } as const, + { + code: ` // issue #367 - example B + import { render } from '${testingFramework}'; + function setup() { return render(
); } - + it('foo', () => { const { getByText } = setup(); expect(getByText('foo')).toBeInTheDocument(); }); - + it('bar', () => { const results = setup(); const { getByText } = results; expect(getByText('foo')).toBe('foo'); }); `, - errors: [ - { - messageId: 'preferScreenQueries', - line: 10, - column: 16, - data: { - name: 'getByText', - }, - }, - { - messageId: 'preferScreenQueries', - line: 16, - column: 16, - data: { - name: 'getByText', - }, - }, - ], - }, - ], + errors: [ + { + messageId: 'preferScreenQueries', + line: 10, + column: 16, + data: { + name: 'getByText', + }, + }, + { + messageId: 'preferScreenQueries', + line: 16, + column: 16, + data: { + name: 'getByText', + }, + }, + ], + } as const, + ]), + ], }); diff --git a/tests/lib/rules/prefer-user-event.test.ts b/tests/lib/rules/prefer-user-event.test.ts index 4b7de9a2..299a65dc 100644 --- a/tests/lib/rules/prefer-user-event.test.ts +++ b/tests/lib/rules/prefer-user-event.test.ts @@ -1,614 +1,615 @@ -import { TSESLint } from '@typescript-eslint/experimental-utils'; +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; import rule, { - MAPPING_TO_USER_EVENT, - MessageIds, - Options, - RULE_NAME, - UserEventMethods, + MAPPING_TO_USER_EVENT, + MessageIds, + Options, + RULE_NAME, + UserEventMethods, } from '../../../lib/rules/prefer-user-event'; import { LIBRARY_MODULES } from '../../../lib/utils'; import { createRuleTester } from '../test-utils'; function createScenarioWithImport< - T extends - | TSESLint.InvalidTestCase - | TSESLint.ValidTestCase + T extends InvalidTestCase | ValidTestCase, >(callback: (libraryModule: string, fireEventMethod: string) => T) { - return LIBRARY_MODULES.reduce( - (acc: Array, libraryModule) => - acc.concat( - Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => - callback(libraryModule, fireEventMethod) - ) - ), - [] - ); + return LIBRARY_MODULES.reduce( + (acc: Array, libraryModule) => + acc.concat( + Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => + callback(libraryModule, fireEventMethod) + ) + ), + [] + ); } const ruleTester = createRuleTester(); function formatUserEventMethodsMessage(fireEventMethod: string): string { - const userEventMethods = MAPPING_TO_USER_EVENT[fireEventMethod].map( - (methodName) => `userEvent.${methodName}` - ); - let joinedList = ''; + const userEventMethods = MAPPING_TO_USER_EVENT[fireEventMethod].map( + (methodName) => `userEvent.${methodName}` + ); + let joinedList = ''; - for (let i = 0; i < userEventMethods.length; i++) { - const item = userEventMethods[i]; - if (i === 0) { - joinedList += item; - } else if (i + 1 === userEventMethods.length) { - joinedList += `, or ${item}`; - } else { - joinedList += `, ${item}`; - } - } + for (let i = 0; i < userEventMethods.length; i++) { + const item = userEventMethods[i]; + if (i === 0) { + joinedList += item; + } else if (i + 1 === userEventMethods.length) { + joinedList += `, or ${item}`; + } else { + joinedList += `, ${item}`; + } + } - return joinedList; + return joinedList; } ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: ` + valid: [ + { + code: ` import { screen } from '@testing-library/user-event' const element = screen.getByText(foo) `, - }, - { - code: ` + }, + { + code: ` const utils = render(baz) const element = utils.getByText(foo) `, - }, - ...UserEventMethods.map((userEventMethod) => ({ - code: ` + }, + ...UserEventMethods.map((userEventMethod) => ({ + code: ` import userEvent from '@testing-library/user-event' const node = document.createElement(elementType) userEvent.${userEventMethod}(foo) `, - })), - ...createScenarioWithImport>( - (libraryModule: string, fireEventMethod: string) => ({ - code: ` + })), + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` import { fireEvent } from '${libraryModule}' const node = document.createElement(elementType) fireEvent.${fireEventMethod}(foo) `, - options: [{ allowedMethods: [fireEventMethod] }], - }) - ), - ...createScenarioWithImport>( - (libraryModule: string, fireEventMethod: string) => ({ - code: ` + options: [{ allowedMethods: [fireEventMethod] }], + }) + ), + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` import { fireEvent as fireEventAliased } from '${libraryModule}' const node = document.createElement(elementType) fireEventAliased.${fireEventMethod}(foo) `, - options: [{ allowedMethods: [fireEventMethod] }], - }) - ), - ...createScenarioWithImport>( - (libraryModule: string, fireEventMethod: string) => ({ - code: ` + options: [{ allowedMethods: [fireEventMethod] }], + }) + ), + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` import * as dom from '${libraryModule}' dom.fireEvent.${fireEventMethod}(foo) `, - options: [{ allowedMethods: [fireEventMethod] }], - }) - ), - ...LIBRARY_MODULES.map((libraryModule) => ({ - // imported fireEvent and not used, - code: ` + options: [{ allowedMethods: [fireEventMethod] }], + }) + ), + ...LIBRARY_MODULES.map((libraryModule) => ({ + // imported fireEvent and not used, + code: ` import { fireEvent } from '${libraryModule}' import * as foo from 'someModule' foo.baz() `, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - // imported dom, but not using fireEvent - code: ` + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + // imported dom, but not using fireEvent + code: ` import * as dom from '${libraryModule}' const button = dom.screen.getByRole('button') const foo = dom.screen.container.querySelector('baz') `, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: ` + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: ` import { fireEvent as aliasedFireEvent } from '${libraryModule}' function fireEvent() { console.log('foo') } fireEvent() `, - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + })), + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { screen } from 'test-utils' const element = screen.getByText(foo) `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { render } from 'test-utils' const utils = render(baz) const element = utils.getByText(foo) `, - }, - ...UserEventMethods.map((userEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + ...UserEventMethods.map((userEventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import userEvent from 'test-utils' const node = document.createElement(elementType) userEvent.${userEventMethod}(foo) `, - })), - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + })), + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` // fireEvent method used but not imported from TL related module // (aggressive reporting opted out) import { fireEvent } from 'somewhere-else' fireEvent.${fireEventMethod}(foo) `, - })), - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + })), + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent } from 'test-utils' const node = document.createElement(elementType) fireEvent.${fireEventMethod}(foo) `, - options: [{ allowedMethods: [fireEventMethod] }], - })), - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + options: [{ allowedMethods: [fireEventMethod] }], + })), + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent as fireEventAliased } from 'test-utils' const node = document.createElement(elementType) fireEventAliased.${fireEventMethod}(foo) `, - options: [{ allowedMethods: [fireEventMethod] }], - })), - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + options: [{ allowedMethods: [fireEventMethod] }], + })), + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import * as dom from 'test-utils' dom.fireEvent.${fireEventMethod}(foo) `, - options: [{ allowedMethods: [fireEventMethod] }], - })), - // edge case for coverage: - // valid use case without call expression - // so there is no innermost function scope found - ` + options: [{ allowedMethods: [fireEventMethod] }], + })), + // edge case for coverage: + // valid use case without call expression + // so there is no innermost function scope found + ` import { fireEvent } from '@testing-library/react'; test('edge case for no innermost function scope', () => { const click = fireEvent.click }) `, - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent, createEvent } from 'test-utils' const event = createEvent.${fireEventMethod}(node) fireEvent(node, event) `, - options: [{ allowedMethods: [fireEventMethod] }], - })), - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + options: [{ allowedMethods: [fireEventMethod] }], + })), + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent as fireEventAliased, createEvent } from 'test-utils' fireEventAliased(node, createEvent.${fireEventMethod}(node)) `, - options: [{ allowedMethods: [fireEventMethod] }], - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + options: [{ allowedMethods: [fireEventMethod] }], + })), + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent, createEvent } from 'test-utils' const event = createEvent.drop(node) fireEvent(node, event) `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent, createEvent } from 'test-utils' const event = createEvent('drop', node) fireEvent(node, event) `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent as fireEventAliased, createEvent as createEventAliased } from 'test-utils' const event = createEventAliased.drop(node) fireEventAliased(node, event) `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent as fireEventAliased, createEvent as createEventAliased } from 'test-utils' const event = createEventAliased('drop', node) fireEventAliased(node, event) `, - }, - { - code: ` + }, + { + code: ` const createEvent = () => 'Event'; const event = createEvent(); `, - }, - ], - invalid: [ - ...createScenarioWithImport>( - (libraryModule: string, fireEventMethod: string) => ({ - code: ` + }, + ], + invalid: [ + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` import { fireEvent } from '${libraryModule}' const node = document.createElement(elementType) fireEvent.${fireEventMethod}(foo) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 4, - column: 9, - data: { - userEventMethods: formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - }, - ], - }) - ), - ...createScenarioWithImport>( - (libraryModule: string, fireEventMethod: string) => ({ - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 4, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + }, + ], + }) + ), + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` import * as dom from '${libraryModule}' dom.fireEvent.${fireEventMethod}(foo) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 3, - column: 9, - data: { - userEventMethods: formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - }, - ], - }) - ), - ...createScenarioWithImport>( - (libraryModule: string, fireEventMethod: string) => ({ - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + }, + ], + }) + ), + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` const { fireEvent } = require('${libraryModule}') fireEvent.${fireEventMethod}(foo) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 3, - column: 9, - data: { - userEventMethods: formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - }, - ], - }) - ), - ...createScenarioWithImport>( - (libraryModule: string, fireEventMethod: string) => ({ - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + }, + ], + }) + ), + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` const rtl = require('${libraryModule}') rtl.fireEvent.${fireEventMethod}(foo) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 3, - column: 9, - data: { - userEventMethods: formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - }, - ], - }) - ), - ...Object.keys(MAPPING_TO_USER_EVENT).map( - (fireEventMethod: string) => - ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + }, + ], + }) + ), + ...Object.keys(MAPPING_TO_USER_EVENT).map( + (fireEventMethod: string) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import * as dom from 'test-utils' dom.fireEvent.${fireEventMethod}(foo) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 3, - column: 9, - data: { - userEventMethods: - formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - }, - ], - } as const) - ), - ...Object.keys(MAPPING_TO_USER_EVENT).map( - (fireEventMethod: string) => - ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: + formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + }, + ], + }) as const + ), + ...Object.keys(MAPPING_TO_USER_EVENT).map( + (fireEventMethod: string) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent } from 'test-utils' fireEvent.${fireEventMethod}(foo) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 3, - column: 9, - data: { - userEventMethods: - formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - }, - ], - } as const) - ), - ...Object.keys(MAPPING_TO_USER_EVENT).map( - (fireEventMethod: string) => - ({ - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: + formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + }, + ], + }) as const + ), + ...Object.keys(MAPPING_TO_USER_EVENT).map( + (fireEventMethod: string) => + ({ + code: ` // same as previous group of test cases but without custom module set // (aggressive reporting) import { fireEvent } from 'test-utils' fireEvent.${fireEventMethod}(foo) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 5, - column: 9, - data: { - userEventMethods: - formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - }, - ], - } as const) - ), - ...Object.keys(MAPPING_TO_USER_EVENT).map( - (fireEventMethod: string) => - ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 5, + column: 9, + data: { + userEventMethods: + formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + }, + ], + }) as const + ), + ...Object.keys(MAPPING_TO_USER_EVENT).map( + (fireEventMethod: string) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent as fireEventAliased } from 'test-utils' fireEventAliased.${fireEventMethod}(foo) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 3, - column: 9, - data: { - userEventMethods: - formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - }, - ], - } as const) - ), - { - code: ` // simple test to check error in detail + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: + formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + }, + ], + }) as const + ), + { + code: ` // simple test to check error in detail import { fireEvent } from '@testing-library/react' fireEvent.click(element) fireEvent.mouseOut(element) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 4, - endLine: 4, - column: 7, - endColumn: 22, - data: { - userEventMethods: - 'userEvent.click, userEvent.type, userEvent.selectOptions, or userEvent.deselectOptions', - fireEventMethod: 'click', - }, - }, - { - messageId: 'preferUserEvent', - line: 5, - endLine: 5, - column: 7, - endColumn: 25, - data: { - userEventMethods: 'userEvent.unhover', - fireEventMethod: 'mouseOut', - }, - }, - ], - }, - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 4, + endLine: 4, + column: 7, + endColumn: 22, + data: { + userEventMethods: + 'userEvent.click, userEvent.type, userEvent.selectOptions, or userEvent.deselectOptions', + fireEventMethod: 'click', + }, + }, + { + messageId: 'preferUserEvent', + line: 5, + endLine: 5, + column: 7, + endColumn: 25, + data: { + userEventMethods: 'userEvent.unhover', + fireEventMethod: 'mouseOut', + }, + }, + ], + }, + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent, createEvent } from 'test-utils' - + fireEvent(node, createEvent('${fireEventMethod}', node)) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 4, - column: 9, - data: { - userEventMethods: formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - } as const, - ], - })), - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 4, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + } as const, + ], + })), + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent, createEvent } from 'test-utils' fireEvent(node, createEvent.${fireEventMethod}(node)) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 4, - column: 9, - data: { - userEventMethods: formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - } as const, - ], - })), - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 4, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + } as const, + ], + })), + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent, createEvent } from 'test-utils' const event = createEvent.${fireEventMethod}(node) fireEvent(node, event) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 4, - column: 9, - data: { - userEventMethods: formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - } as const, - ], - })), - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 4, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + } as const, + ], + })), + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent as fireEventAliased, createEvent as createEventAliased } from 'test-utils' const eventValid = createEventAliased.drop(node) fireEventAliased(node, eventValid) const eventInvalid = createEventAliased.${fireEventMethod}(node) fireEventAliased(node, eventInvalid) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 6, - column: 9, - data: { - userEventMethods: formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - } as const, - ], - })), - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 6, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + } as const, + ], + })), + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import * as dom from 'test-utils' const eventValid = dom.createEvent.drop(node) dom.fireEvent(node, eventValid) const eventInvalid = dom.createEvent.${fireEventMethod}(node) dom.fireEvent(node, eventInvalid) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 6, - column: 9, - data: { - userEventMethods: formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - } as const, - ], - })), - ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + messageId: 'preferUserEvent', + line: 6, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + } as const, + ], + })), + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import * as dom from 'test-utils' // valid event dom.fireEvent(node, dom.createEvent.drop(node)) // invalid event dom.fireEvent(node, dom.createEvent.${fireEventMethod}(node)) `, - errors: [ - { - messageId: 'preferUserEvent', - line: 6, - column: 9, - data: { - userEventMethods: formatUserEventMethodsMessage(fireEventMethod), - fireEventMethod, - }, - } as const, - ], - })), - ], + errors: [ + { + messageId: 'preferUserEvent', + line: 6, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod, + }, + } as const, + ], + })), + ], }); diff --git a/tests/lib/rules/prefer-wait-for.test.ts b/tests/lib/rules/prefer-wait-for.test.ts deleted file mode 100644 index ae6a85c4..00000000 --- a/tests/lib/rules/prefer-wait-for.test.ts +++ /dev/null @@ -1,2258 +0,0 @@ -import rule, { RULE_NAME } from '../../../lib/rules/prefer-wait-for'; -import { LIBRARY_MODULES } from '../../../lib/utils'; -import { createRuleTester } from '../test-utils'; - -const ruleTester = createRuleTester(); - -ruleTester.run(RULE_NAME, rule, { - valid: [ - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { waitFor, render } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { waitFor, render } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - }`, - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitFor, render } from 'test-utils'; - - async () => { - await waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitFor, render } = require('test-utils'); - - async () => { - await waitFor(() => {}); - }`, - }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { waitForElementToBeRemoved, render } from '${libraryModule}'; - - async () => { - await waitForElementToBeRemoved(() => {}); - }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { waitForElementToBeRemoved, render } = require('${libraryModule}'); - - async () => { - await waitForElementToBeRemoved(() => {}); - }`, - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForElementToBeRemoved, render } from 'test-utils'; - - async () => { - await waitForElementToBeRemoved(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForElementToBeRemoved, render } = require('test-utils'); - - async () => { - await waitForElementToBeRemoved(() => {}); - }`, - }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.waitForElementToBeRemoved(() => {}); - }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.waitForElementToBeRemoved(() => {}); - }`, - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.waitForElementToBeRemoved(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.waitForElementToBeRemoved(() => {}); - }`, - }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { render } from '${libraryModule}'; - import { waitForSomethingElse } from 'other-module'; - - async () => { - await waitForSomethingElse(() => {}); - }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { render } = require('${libraryModule}'); - const { waitForSomethingElse } = require('other-module'); - - async () => { - await waitForSomethingElse(() => {}); - }`, - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { render } from 'test-utils'; - import { waitForSomethingElse } from 'other-module'; - - async () => { - await waitForSomethingElse(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { render } = require('test-utils'); - const { waitForSomethingElse } = require('other-module'); - - async () => { - await waitForSomethingElse(() => {}); - }`, - }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - }, - { - code: `import { wait } from 'imNoTestingLibrary'; - - async () => { - await wait(); - }`, - }, - { - code: `const { wait } = require('imNoTestingLibrary'); - - async () => { - await wait(); - }`, - }, - { - code: `import * as foo from 'imNoTestingLibrary'; - - async () => { - await foo.wait(); - }`, - }, - { - code: `const foo = require('imNoTestingLibrary'); - - async () => { - await foo.wait(); - }`, - }, - { - code: `import * as foo from 'imNoTestingLibrary'; - cy.wait(); - `, - }, - { - code: `const foo = require('imNoTestingLibrary'); - cy.wait(); - `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: aggressive reporting disabled - method named same as invalid method - // but not coming from Testing Library is valid - import { wait as testingLibraryWait } from 'test-utils' - import { wait } from 'somewhere-else' - - async () => { - await wait(); - } - `, - }, - { - // https://github.com/testing-library/eslint-plugin-testing-library/issues/145 - code: `import * as foo from 'imNoTestingLibrary'; - async function wait(): Promise { - // doesn't matter - } - - function callsWait(): void { - await wait(); - } - `, - }, - { - // https://github.com/testing-library/eslint-plugin-testing-library/issues/145 - code: `const foo = require('imNoTestingLibrary'); - async function wait(): Promise { - // doesn't matter - } - - function callsWait(): void { - await wait(); - } - `, - }, - ], - - invalid: [ - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { wait, render } from '${libraryModule}'; - - async () => { - await wait(); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { wait, render } = require('${libraryModule}'); - - async () => { - await wait(); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { wait, render } from 'test-utils'; - - async () => { - await wait(); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { wait, render } = require('test-utils'); - - async () => { - await wait(); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}); - }`, - }, - // namespaced wait should be fixed but not its import - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.wait(); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.waitFor(() => {}); - }`, - } as const) - ), - // namespaced wait should be fixed but not its import - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.wait(); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.waitFor(() => {}); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.wait(); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.wait(); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.waitFor(() => {}); - }`, - }, - // namespaced waitForDomChange should be fixed but not its import - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.waitForDomChange({ timeout: 500 }); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - } as const) - ), - // namespaced waitForDomChange should be fixed but not its import - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.waitForDomChange({ timeout: 500 }); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.waitForDomChange({ timeout: 500 }); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.waitForDomChange({ timeout: 500 }); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { render, wait } from '${libraryModule}' - - async () => { - await wait(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { render, wait } = require('${libraryModule}'); - - async () => { - await wait(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { render, wait } from 'test-utils' - - async () => { - await wait(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { render, wait } = require('test-utils'); - - async () => { - await wait(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}); - }`, - }, - // this import doesn't have trailing semicolon but fixer adds it - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { render, wait, screen } from "${libraryModule}"; - - async () => { - await wait(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - } as const) - ), - // this import doesn't have trailing semicolon but fixer adds it - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { render, wait, screen } from "${libraryModule}"; - - async () => { - await wait(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { render, wait, screen } from "test-utils"; - - async () => { - await wait(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from 'test-utils'; - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { render, wait, screen } = require('test-utils'); - - async () => { - await wait(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,screen,waitFor } = require('test-utils'); - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { render, waitForElement, screen } from '${libraryModule}' - - async () => { - await waitForElement(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { render, waitForElement, screen } = require('${libraryModule}'); - - async () => { - await waitForElement(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,screen,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { render, waitForElement, screen } from 'test-utils' - - async () => { - await waitForElement(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { render, waitForElement, screen } = require('test-utils'); - - async () => { - await waitForElement(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,screen,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForElement } from '${libraryModule}'; - - async () => { - await waitForElement(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForElement } = require('${libraryModule}'); - - async () => { - await waitForElement(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForElement } from 'test-utils'; - - async () => { - await waitForElement(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from 'test-utils'; - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForElement } = require('test-utils'); - - async () => { - await waitForElement(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('test-utils'); - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForDomChange } from '${libraryModule}'; - - async () => { - await waitForDomChange(); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForDomChange } = require('${libraryModule}'); - - async () => { - await waitForDomChange(); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForDomChange } from 'test-utils'; - - async () => { - await waitForDomChange(); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForDomChange } = require('test-utils'); - - async () => { - await waitForDomChange(); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForDomChange } from '${libraryModule}'; - - async () => { - await waitForDomChange(mutationObserverOptions); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}, mutationObserverOptions); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForDomChange } = require('${libraryModule}'); - - async () => { - await waitForDomChange(mutationObserverOptions); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}, mutationObserverOptions); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForDomChange } from 'test-utils'; - - async () => { - await waitForDomChange(mutationObserverOptions); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}, mutationObserverOptions); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForDomChange } = require('test-utils'); - - async () => { - await waitForDomChange(mutationObserverOptions); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}, mutationObserverOptions); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForDomChange } from '${libraryModule}'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForDomChange } = require('${libraryModule}'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForDomChange } from 'test-utils'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForDomChange } = require('test-utils'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForDomChange, wait, waitForElement } from '${libraryModule}'; - import userEvent from '@testing-library/user-event'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 8, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; - import userEvent from '@testing-library/user-event'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForDomChange, wait, waitForElement } = require('${libraryModule}'); - const userEvent = require('@testing-library/user-event'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 8, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); - const userEvent = require('@testing-library/user-event'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForDomChange, wait, waitForElement } from 'test-utils'; - import userEvent from '@testing-library/user-event'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 8, - column: 15, - }, - ], - output: `import { waitFor } from 'test-utils'; - import userEvent from '@testing-library/user-event'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForDomChange, wait, waitForElement } = require('test-utils'); - const userEvent = require('@testing-library/user-event'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 8, - column: 15, - }, - ], - output: `const { waitFor } = require('test-utils'); - const userEvent = require('@testing-library/user-event'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { render, waitForDomChange, wait, waitForElement } from '${libraryModule}'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { render, waitForDomChange, wait, waitForElement } = require('${libraryModule}'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { render, waitForDomChange, wait, waitForElement } from 'test-utils'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { render, waitForDomChange, wait, waitForElement } = require('test-utils'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForDomChange, wait, render, waitForElement } from '${libraryModule}'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForDomChange, wait, render, waitForElement } = require('${libraryModule}'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForDomChange, wait, render, waitForElement } from 'test-utils'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForDomChange, wait, render, waitForElement } = require('test-utils'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { - waitForDomChange, - wait, - render, - waitForElement, - } from '${libraryModule}'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 9, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 10, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 11, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 12, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { - waitForDomChange, - wait, - render, - waitForElement, - } = require('${libraryModule}'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 9, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 10, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 11, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 12, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { - waitForDomChange, - wait, - render, - waitForElement, - } from 'test-utils'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 9, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 10, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 11, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 12, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { - waitForDomChange, - wait, - render, - waitForElement, - } = require('test-utils'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 9, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 10, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 11, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 12, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - // if already importing waitFor then it's not imported twice - code: `import { wait, waitFor, render } from '${libraryModule}'; - - async () => { - await wait(); - await waitFor(someCallback); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - await waitFor(someCallback); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - // if already importing waitFor then it's not imported twice - code: `const { wait, waitFor, render } = require('${libraryModule}'); - - async () => { - await wait(); - await waitFor(someCallback); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - await waitFor(someCallback); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - // if already importing waitFor then it's not imported twice - code: `import { wait, waitFor, render } from 'test-utils'; - - async () => { - await wait(); - await waitFor(someCallback); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}); - await waitFor(someCallback); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - // if already importing waitFor then it's not imported twice - code: `const { wait, waitFor, render } = require('test-utils'); - - async () => { - await wait(); - await waitFor(someCallback); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}); - await waitFor(someCallback); - }`, - }, - ], -}); diff --git a/tests/lib/rules/render-result-naming-convention.test.ts b/tests/lib/rules/render-result-naming-convention.test.ts index 51b9e023..b2715300 100644 --- a/tests/lib/rules/render-result-naming-convention.test.ts +++ b/tests/lib/rules/render-result-naming-convention.test.ts @@ -1,126 +1,136 @@ import rule, { - RULE_NAME, + RULE_NAME, } from '../../../lib/rules/render-result-naming-convention'; import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: ` - import { render } from '@testing-library/react'; - - test('should not report straight destructured render result', () => { - const { rerender, getByText } = render(); - const button = getByText('some button'); - }); - `, - }, - { - code: ` - import * as RTL from '@testing-library/react'; - - test('should not report straight destructured render result from wildcard import', () => { - const { rerender, getByText } = RTL.render(); - const button = getByText('some button'); - }); - `, - }, - { - code: ` - import { render } from '@testing-library/react'; - - test('should not report straight render result called "utils"', async () => { - const utils = render(); - await utils.findByRole('button'); - }); - `, - }, - { - code: ` - import { render } from '@testing-library/react'; - - test('should not report straight render result called "view"', async () => { - const view = render(); - await view.findByRole('button'); - }); - `, - }, - { - code: ` - import { render } from '@testing-library/react'; - - const setup = () => render(); - - test('should not report destructured render result from wrapping function', () => { - const { rerender, getByText } = setup(); - const button = getByText('some button'); - }); - `, - }, - { - code: ` - import { render } from '@testing-library/react'; - - const setup = () => render(); - - test('should not report render result called "utils" from wrapping function', async () => { - const utils = setup(); - await utils.findByRole('button'); - }); - `, - }, - { - code: ` - import { render } from '@testing-library/react'; - - const setup = () => render(); - - test('should not report render result called "view" from wrapping function', async () => { - const view = setup(); - await view.findByRole('button'); - }); - `, - }, - { - code: ` - import { screen } from '@testing-library/react'; - import { customRender } from 'test-utils'; - - test('should not report straight destructured render result from custom render', () => { - const { unmount } = customRender(); - const button = screen.getByText('some button'); - }); - `, - settings: { 'testing-library/custom-renders': ['customRender'] }, - }, - { - code: ` + valid: [ + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { render } from '${testingFramework}'; + + test('should not report straight destructured render result', () => { + const { rerender, getByText } = render(); + const button = getByText('some button'); + }); + `, + }, + { + code: ` + import * as RTL from '${testingFramework}'; + + test('should not report straight destructured render result from wildcard import', () => { + const { rerender, getByText } = RTL.render(); + const button = getByText('some button'); + }); + `, + }, + { + code: ` + import { render } from '${testingFramework}'; + + test('should not report straight render result called "utils"', async () => { + const utils = render(); + await utils.findByRole('button'); + }); + `, + }, + { + code: ` + import { render } from '${testingFramework}'; + + test('should not report straight render result called "view"', async () => { + const view = render(); + await view.findByRole('button'); + }); + `, + }, + { + code: ` + import { render } from '${testingFramework}'; + + const setup = () => render(); + + test('should not report destructured render result from wrapping function', () => { + const { rerender, getByText } = setup(); + const button = getByText('some button'); + }); + `, + }, + { + code: ` + import { render } from '${testingFramework}'; + + const setup = () => render(); + + test('should not report render result called "utils" from wrapping function', async () => { + const utils = setup(); + await utils.findByRole('button'); + }); + `, + }, + { + code: ` + import { render } from '${testingFramework}'; + + const setup = () => render(); + + test('should not report render result called "view" from wrapping function', async () => { + const view = setup(); + await view.findByRole('button'); + }); + `, + }, + { + code: ` + import { screen } from '${testingFramework}'; + import { customRender } from 'test-utils'; + + test('should not report straight destructured render result from custom render', () => { + const { unmount } = customRender(); + const button = screen.getByText('some button'); + }); + `, + settings: { 'testing-library/custom-renders': ['customRender'] }, + }, + ]), + { + code: ` import { customRender } from 'test-utils'; - + test('should not report render result called "view" from custom render', async () => { const view = customRender(); await view.findByRole('button'); }); `, - settings: { 'testing-library/custom-renders': ['customRender'] }, - }, - { - code: ` + settings: { 'testing-library/custom-renders': ['customRender'] }, + }, + { + code: ` import { customRender } from 'test-utils'; - + test('should not report render result called "utils" from custom render', async () => { const utils = customRender(); await utils.findByRole('button'); }); `, - settings: { 'testing-library/custom-renders': ['customRender'] }, - }, - { - code: ` - import { render } from '@testing-library/react'; - + settings: { 'testing-library/custom-renders': ['customRender'] }, + }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { render } from '${testingFramework}'; + const setup = () => { // this one must have a valid name const view = render(); @@ -133,13 +143,13 @@ ruleTester.run(RULE_NAME, rule, { await wrapper.findByRole('button'); }); `, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render as testingLibraryRender } from '@testing-library/react'; + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingLibraryRender } from '${testingFramework}'; import { render } from '@somewhere/else' - + const setup = () => render(); test('aggressive reporting disabled - should not report nested render not related to TL', () => { @@ -147,16 +157,17 @@ ruleTester.run(RULE_NAME, rule, { const button = wrapper.getByText('some button'); }); `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - 'testing-library/custom-renders': ['customRender'], - }, - code: ` + }, + ]), + { + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['customRender'], + }, + code: ` import { customRender as myRender } from 'test-utils'; import { customRender } from 'non-related' - + const setup = () => { return customRender(); }; @@ -168,16 +179,16 @@ ruleTester.run(RULE_NAME, rule, { await wrapper.findByRole('button'); }); `, - }, - { - settings: { - 'testing-library/utils-module': 'off', - 'testing-library/custom-renders': 'off', - }, - code: ` + }, + { + settings: { + 'testing-library/utils-module': 'off', + 'testing-library/custom-renders': 'off', + }, + code: ` import { customRender as myRender } from 'test-utils'; import { render } from 'non-related' - + const setup = () => { return render(); }; @@ -192,110 +203,111 @@ ruleTester.run(RULE_NAME, rule, { await wrapper1.findByRole('button'); }); `, - }, - ], - invalid: [ - { - code: ` - import { render } from '@testing-library/react'; - + }, + ], + invalid: [ + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { render } from '${testingFramework}'; + test('should report straight render result called "wrapper"', async () => { const wrapper = render(); await wrapper.findByRole('button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 5, - column: 17, - }, - ], - }, - { - code: ` - import * as RTL from '@testing-library/react'; - + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 5, + column: 17, + }, + ], + } as const, + { + code: ` + import * as RTL from '${testingFramework}'; + test('should report straight render result called "wrapper" from wildcard import', () => { const wrapper = RTL.render(); const button = wrapper.getByText('some button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 5, - column: 17, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; - + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 5, + column: 17, + }, + ], + } as const, + { + code: ` + import { render } from '${testingFramework}'; + test('should report straight render result called "component"', async () => { const component = render(); await component.findByRole('button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'component', - }, - line: 5, - column: 17, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; - + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'component', + }, + line: 5, + column: 17, + }, + ], + } as const, + { + code: ` + import { render } from '${testingFramework}'; + test('should report straight render result called "notValidName"', async () => { const notValidName = render(); await notValidName.findByRole('button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - line: 5, - column: 17, - }, - ], - }, - { - code: ` - import { render as testingLibraryRender } from '@testing-library/react'; - + errors: [ + { + messageId: 'renderResultNamingConvention', + line: 5, + column: 17, + }, + ], + } as const, + { + code: ` + import { render as testingLibraryRender } from '${testingFramework}'; + test('should report renamed render result called "wrapper"', async () => { const wrapper = testingLibraryRender(); await wrapper.findByRole('button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 5, - column: 17, - }, - ], - }, - { - code: ` - import { render } from '@testing-library/react'; - + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 5, + column: 17, + }, + ], + } as const, + { + code: ` + import { render } from '${testingFramework}'; + const setup = () => { // this one must have a valid name const wrapper = render(); @@ -308,22 +320,22 @@ ruleTester.run(RULE_NAME, rule, { await wrapper.findByRole('button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 6, - column: 17, - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from '@testing-library/react'; - + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 6, + column: 17, + }, + ], + } as const, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}'; + const setup = () => render(); test('aggressive reporting disabled - should report nested render from TL package', () => { @@ -331,22 +343,23 @@ ruleTester.run(RULE_NAME, rule, { const button = wrapper.getByText('some button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 7, - column: 17, - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 7, + column: 17, + }, + ], + } as const, + ]), + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { render } from 'test-utils'; - + function setup() { doSomethingElse(); return render() @@ -357,19 +370,19 @@ ruleTester.run(RULE_NAME, rule, { const button = wrapper.getByText('some button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 10, - column: 17, - }, - ], - }, - { - code: ` + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 10, + column: 17, + }, + ], + }, + { + code: ` import { customRender } from 'test-utils'; test('should report from custom render function ', () => { @@ -377,20 +390,20 @@ ruleTester.run(RULE_NAME, rule, { const button = wrapper.getByText('some button'); }); `, - settings: { 'testing-library/custom-renders': ['customRender'] }, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 5, - column: 17, - }, - ], - }, - { - code: ` + settings: { 'testing-library/custom-renders': ['customRender'] }, + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 5, + column: 17, + }, + ], + }, + { + code: ` import { render } from '@foo/bar'; test('aggressive reporting - should report from render not related to testing library', () => { @@ -398,19 +411,19 @@ ruleTester.run(RULE_NAME, rule, { const button = wrapper.getByText('some button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 5, - column: 17, - }, - ], - }, - { - code: ` + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 5, + column: 17, + }, + ], + }, + { + code: ` import * as RTL from '@foo/bar'; test('aggressive reporting - should report from wildcard render not imported from testing library', () => { @@ -418,19 +431,19 @@ ruleTester.run(RULE_NAME, rule, { const button = wrapper.getByText('some button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 5, - column: 17, - }, - ], - }, - { - code: ` + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 5, + column: 17, + }, + ], + }, + { + code: ` function render() { return 'whatever'; } @@ -440,21 +453,22 @@ ruleTester.run(RULE_NAME, rule, { const button = wrapper.getByText('some button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 7, - column: 17, - }, - ], - }, - { - code: ` - import { render as testingLibraryRender } from '@testing-library/react'; - + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 7, + column: 17, + }, + ], + }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ + { + code: ` + import { render as testingLibraryRender } from '${testingFramework}'; + const setup = () => { return testingLibraryRender(); }; @@ -464,22 +478,22 @@ ruleTester.run(RULE_NAME, rule, { await wrapper.findByRole('button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 9, - column: 17, - }, - ], - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render as testingLibraryRender } from '@testing-library/react'; - + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 9, + column: 17, + }, + ], + } as const, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingLibraryRender } from '${testingFramework}'; + const setup = () => { return testingLibraryRender(); }; @@ -491,25 +505,26 @@ ruleTester.run(RULE_NAME, rule, { await wrapper.findByRole('button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 11, - column: 17, - }, - ], - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - 'testing-library/custom-renders': ['customRender'], - }, - code: ` + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 11, + column: 17, + }, + ], + } as const, + ]), + { + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['customRender'], + }, + code: ` import { customRender as myRender } from 'test-utils'; - + const setup = () => { return myRender(); }; @@ -521,16 +536,16 @@ ruleTester.run(RULE_NAME, rule, { await wrapper.findByRole('button'); }); `, - errors: [ - { - messageId: 'renderResultNamingConvention', - data: { - renderResultName: 'wrapper', - }, - line: 11, - column: 17, - }, - ], - }, - ], + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 11, + column: 17, + }, + ], + }, + ], }); diff --git a/tests/lib/test-utils.ts b/tests/lib/test-utils.ts index 4c7cbf46..c65ac6c1 100644 --- a/tests/lib/test-utils.ts +++ b/tests/lib/test-utils.ts @@ -1,50 +1,45 @@ -import { resolve } from 'path'; +import tsESLintParser from '@typescript-eslint/parser'; +import { RuleTester, RunTests } from '@typescript-eslint/rule-tester'; -import { TSESLint } from '@typescript-eslint/experimental-utils'; +import { TestingLibraryPluginRuleModule } from '../../lib/utils'; const DEFAULT_TEST_CASE_CONFIG = { - filename: 'MyComponent.test.js', + filename: 'MyComponent.test.js', }; -class TestingLibraryRuleTester extends TSESLint.RuleTester { - run>( - ruleName: string, - rule: TSESLint.RuleModule, - tests: TSESLint.RunTests - ): void { - const { valid, invalid } = tests; +class TestingLibraryRuleTester extends RuleTester { + run( + ruleName: string, + rule: TestingLibraryPluginRuleModule, + { invalid, valid }: RunTests + ): void { + const finalValid = valid.map((testCase) => { + if (typeof testCase === 'string') { + return { + ...DEFAULT_TEST_CASE_CONFIG, + code: testCase, + }; + } - const finalValid = valid.map((testCase) => { - if (typeof testCase === 'string') { - return { - ...DEFAULT_TEST_CASE_CONFIG, - code: testCase, - }; - } + return { ...DEFAULT_TEST_CASE_CONFIG, ...testCase }; + }); + const finalInvalid = invalid.map((testCase) => ({ + ...DEFAULT_TEST_CASE_CONFIG, + ...testCase, + })); - return { ...DEFAULT_TEST_CASE_CONFIG, ...testCase }; - }); - const finalInvalid = invalid.map((testCase) => ({ - ...DEFAULT_TEST_CASE_CONFIG, - ...testCase, - })); - - super.run(ruleName, rule, { valid: finalValid, invalid: finalInvalid }); - } + super.run(ruleName, rule, { valid: finalValid, invalid: finalInvalid }); + } } -export const createRuleTester = ( - parserOptions: Partial = {} -): TSESLint.RuleTester => { - return new TestingLibraryRuleTester({ - parser: resolve('./node_modules/@typescript-eslint/parser'), - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - ...parserOptions, - }, - }); -}; +export const createRuleTester = () => + new TestingLibraryRuleTester({ + languageOptions: { + parser: tsESLintParser, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + }); diff --git a/tests/lib/utils/is-testing-library-module.test.ts b/tests/lib/utils/is-testing-library-module.test.ts new file mode 100644 index 00000000..850e6a65 --- /dev/null +++ b/tests/lib/utils/is-testing-library-module.test.ts @@ -0,0 +1,96 @@ +import { + isCustomTestingLibraryModule, + isOfficialTestingLibraryModule, + isTestingLibraryModule, +} from '../../../lib/utils/is-testing-library-module'; + +const OLD_LIBRARY_MODULES = [ + 'dom-testing-library', + 'vue-testing-library', + 'react-testing-library', +] as const; + +const LIBRARY_MODULES = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/preact', + '@testing-library/vue', + '@testing-library/svelte', + '@marko/testing-library', +] as const; + +const USER_EVENT_MODULE = '@testing-library/user-event'; + +describe('isOfficialTestingLibraryModule', () => { + it.each([...OLD_LIBRARY_MODULES, ...LIBRARY_MODULES, USER_EVENT_MODULE])( + 'returns true when arg is "%s"', + (importSourceName) => { + const result = isOfficialTestingLibraryModule(importSourceName); + + expect(result).toBe(true); + } + ); + + it.each(['custom-modules', 'hoge-testing-library', '@testing-library/hoge'])( + 'returns false when arg is "%s"', + (importSourceName) => { + const result = isOfficialTestingLibraryModule(importSourceName); + + expect(result).toBe(false); + } + ); +}); + +describe('isCustomTestingLibraryModule', () => { + it.each(['test-utils', '../test-utils', '@/test-utils'])( + 'returns true when arg is "%s"', + (importSourceName) => { + const result = isCustomTestingLibraryModule( + importSourceName, + 'test-utils' + ); + + expect(result).toBe(true); + } + ); + + it.each([ + 'custom-modules', + 'react-testing-library', + '@testing-library/react', + 'test-util', + 'test-utils-module', + ])('returns false when arg is "%s"', (importSourceName) => { + const result = isCustomTestingLibraryModule(importSourceName, 'test-utils'); + + expect(result).toBe(false); + }); +}); + +describe('isTestingLibraryModule', () => { + it.each([ + ...OLD_LIBRARY_MODULES, + ...LIBRARY_MODULES, + USER_EVENT_MODULE, + 'test-utils', + '../test-utils', + '@/test-utils', + ])('returns true when arg is "%s"', (importSourceName) => { + const result = isTestingLibraryModule(importSourceName, 'test-utils'); + + expect(result).toBe(true); + }); + + it.each([ + 'custom-modules', + 'hoge-testing-library', + '@testing-library/hoge', + 'test-util', + 'test-utils-module', + ])('returns false when arg is "%s"', (importSourceName) => { + const result = isTestingLibraryModule(importSourceName, 'test-utils'); + + expect(result).toBe(false); + }); +}); diff --git a/tests/lib/utils/resolve-to-testing-library-fn.test.ts b/tests/lib/utils/resolve-to-testing-library-fn.test.ts new file mode 100644 index 00000000..c8c026a9 --- /dev/null +++ b/tests/lib/utils/resolve-to-testing-library-fn.test.ts @@ -0,0 +1,417 @@ +import { InvalidTestCase } from '@typescript-eslint/rule-tester'; + +import { createTestingLibraryRule } from '../../../lib/create-testing-library-rule'; +import { LIBRARY_MODULES } from '../../../lib/utils'; +import { resolveToTestingLibraryFn } from '../../../lib/utils/resolve-to-testing-library-fn'; +import { createRuleTester } from '../test-utils'; + +type MessageIds = 'details'; + +const rule = createTestingLibraryRule<[], MessageIds>({ + name: __filename, + meta: { + docs: { + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + description: 'Fake rule for testing parseUserEventFnCall', + }, + messages: { + details: '{{ data }}', + }, + schema: [], + type: 'problem', + }, + defaultOptions: [], + create: (context) => ({ + CallExpression(node) { + const testingLibraryFn = resolveToTestingLibraryFn(node, context); + + if (testingLibraryFn) { + context.report({ + messageId: 'details', + node, + data: { + data: testingLibraryFn, + }, + }); + } + }, + }), +}); + +const ruleTester = createRuleTester(); + +ruleTester.run('esm', rule, { + valid: [ + { + code: ` + import { userEvent } from './test-utils'; + + (userEvent => userEvent.setup)(); + `, + }, + { + code: ` + import { userEvent } from './test-utils'; + + function userClick() { + userEvent.click(document.body); + } + [].forEach(userClick); + `, + }, + { + code: ` + import { userEvent } from './test-utils'; + + userEvent.setup() + `, + }, + { + code: ` + import * as userEvent from '@testing-library/user-event'; + + userEvent.default.setup() + `, + }, + ...LIBRARY_MODULES.map((module) => ({ + code: ` + import * as testingLibrary from '${module}'; + + const { fireEvent } = testingLibrary + fireEvent.click(document.body) + `, + })), + ], + invalid: [ + { + code: ` + import userEvent from '@testing-library/user-event'; + + userEvent.setup() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: null, + local: 'userEvent', + }, + }, + }, + ], + }, + { + code: ` + const { userEvent } = await import('@testing-library/user-event'); + + userEvent.setup() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: null, + local: 'userEvent', + }, + }, + }, + ], + }, + { + code: ` + import { "userEvent" as user } from '@testing-library/user-event'; + + user.setup() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: null, + local: 'userEvent', + }, + }, + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import userEvent from 'test-utils'; + + userEvent.setup() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: null, + local: 'userEvent', + }, + }, + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import userEvent from '../test-utils'; + + userEvent.setup() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: null, + local: 'userEvent', + }, + }, + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import userEvent from '@/test-utils'; + + userEvent.setup() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: null, + local: 'userEvent', + }, + }, + }, + ], + }, + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { customRender } from 'test-utils'; + + customRender() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: null, + local: 'customRender', + }, + }, + }, + ], + }, + { + settings: { + 'testing-library/custom-queries': ['ByComplexText', 'queryByIcon'], + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { queryByIcon } from 'test-utils'; + + queryByIcon() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: null, + local: 'queryByIcon', + }, + }, + }, + ], + }, + ...LIBRARY_MODULES.flatMap>((module) => [ + { + code: ` + import { fireEvent } from '${module}'; + + fireEvent.click(document.body) + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: 'fireEvent', + local: 'fireEvent', + }, + }, + }, + ], + }, + { + code: ` + import { fireEvent as fe } from '${module}'; + + fe.click(document.body) + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: 'fireEvent', + local: 'fe', + }, + }, + }, + ], + }, + ]), + ], +}); + +ruleTester.run('cjs', rule, { + valid: [ + { + code: ` + const { userEvent } = require('./test-utils'); + + userEvent.setup() + `, + }, + { + code: ` + const { "default": userEvent } = require('./test-utils'); + + userEvent.setup() + `, + }, + { + code: ` + const { userEvent } = require(\`./test-utils\`); + + userEvent.setup() + `, + }, + ...LIBRARY_MODULES.map((module) => ({ + code: ` + const testingLibrary = require('${module}'); + + const { fireEvent } = testingLibrary + fireEvent.click(document.body) + `, + })), + ], + invalid: [ + { + code: ` + const { default: userEvent } = require('@testing-library/user-event'); + + userEvent.setup() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: 'default', + local: 'userEvent', + }, + }, + }, + ], + }, + { + code: ` + const { default: userEvent } = require(\`@testing-library/user-event\`); + + userEvent.setup() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: 'default', + local: 'userEvent', + }, + }, + }, + ], + }, + ...LIBRARY_MODULES.flatMap>((module) => [ + { + code: ` + const { fireEvent } = require('${module}'); + + fireEvent.click(document.body) + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: 'fireEvent', + local: 'fireEvent', + }, + }, + }, + ], + }, + { + code: ` + const { fireEvent: fe } = require('${module}'); + + fe.click(document.body) + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: 'fireEvent', + local: 'fe', + }, + }, + }, + ], + }, + ]), + ], +}); + +ruleTester.run('typescript', rule, { + valid: [ + { + code: ` + import userEvent = require('@testing-library/user-event'); + + userEvent.setup() + `, + }, + { + code: ` + import type { userEvent } from '@testing-library/user-event'; + + userEvent.setup() + `, + }, + ], + invalid: [], +}); diff --git a/tools/generate-configs/index.ts b/tools/generate-configs/index.ts index f36a9051..db460548 100644 --- a/tools/generate-configs/index.ts +++ b/tools/generate-configs/index.ts @@ -1,41 +1,41 @@ -import type { LinterConfigRules } from '../../lib/configs'; +import { type TSESLint } from '@typescript-eslint/utils'; + import rules from '../../lib/rules'; import { - SUPPORTED_TESTING_FRAMEWORKS, - SupportedTestingFramework, + SUPPORTED_TESTING_FRAMEWORKS, + SupportedTestingFramework, + TestingLibraryPluginRuleModule, } from '../../lib/utils'; -import { LinterConfig, writeConfig } from './utils'; +import { 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]; + framework: SupportedTestingFramework +): Record => + Object.entries>(rules) + .filter(([_, { meta }]) => Boolean(meta.docs.recommendedConfig[framework])) + .reduce((allRules, [ruleName, { meta }]) => { + const name = `${RULE_NAME_PREFIX}${ruleName}`; + const recommendation = meta.docs.recommendedConfig[framework]; - return { - ...allRules, - [name]: recommendation, - }; - }, {}); + return { + ...allRules, + [name]: recommendation, + }; + }, {}); -SUPPORTED_TESTING_FRAMEWORKS.forEach((framework) => { - const specificFrameworkConfig: LinterConfig = { - plugins: ['testing-library'], - rules: getRecommendedRulesForTestingFramework(framework), - }; +(async () => { + for (const framework of SUPPORTED_TESTING_FRAMEWORKS) { + const specificFrameworkConfig: TSESLint.Linter.ConfigType = { + plugins: ['testing-library'], + rules: getRecommendedRulesForTestingFramework(framework), + }; - writeConfig(specificFrameworkConfig, framework); + await writeConfig(specificFrameworkConfig, framework); + } +})().catch((error) => { + console.error(error); + process.exitCode = 1; }); diff --git a/tools/generate-configs/utils.ts b/tools/generate-configs/utils.ts index 0394c044..3bddaf5f 100644 --- a/tools/generate-configs/utils.ts +++ b/tools/generate-configs/utils.ts @@ -1,33 +1,34 @@ -import { writeFileSync } from 'fs'; +import { writeFile } from 'fs/promises'; import { resolve } from 'path'; -import type { TSESLint } from '@typescript-eslint/experimental-utils'; +import { type TSESLint } from '@typescript-eslint/utils'; import { format, resolveConfig } from 'prettier'; -const prettierConfig = resolveConfig.sync(__dirname); - -export type LinterConfig = TSESLint.Linter.Config; +const prettierConfig = resolveConfig(__dirname); 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'); + [ + '// THIS CODE WAS AUTOMATICALLY GENERATED', + '// DO NOT EDIT THIS CODE BY HAND', + '// YOU CAN REGENERATE IT USING pnpm 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`); +export const writeConfig = async ( + config: TSESLint.Linter.ConfigType, + configName: string +): Promise => { + // note: we use `export =` because ESLint will import these configs via a commonjs import + const code = `export = ${JSON.stringify(config)};`; + const configStr = await format(addAutoGeneratedComment(code), { + parser: 'typescript', + ...(await prettierConfig), + }); + const filePath = resolve(__dirname, `../../lib/configs/${configName}.ts`); - writeFileSync(filePath, configStr); + await writeFile(filePath, configStr); }; diff --git a/tools/generate-rules-list/index.ts b/tools/generate-rules-list/index.ts deleted file mode 100644 index bd201f13..00000000 --- a/tools/generate-rules-list/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import rules from '../../lib/rules'; -import type { TestingLibraryRuleMetaDocs } from '../../lib/utils'; - -import { configBadges, emojiKey, RulesList, writeRulesList } from './utils'; - -export const createRuleLink = (ruleName: string): string => - `[\`testing-library/${ruleName}\`](./docs/rules/${ruleName}.md)`; - -export const generateConfigBadges = ( - recommendedConfig: TestingLibraryRuleMetaDocs['recommendedConfig'] -): string => - Object.entries(recommendedConfig) - .filter(([_, config]) => Boolean(config)) - .map(([framework]) => configBadges[framework]) - .join(' '); - -const rulesList: RulesList = Object.entries(rules) - .sort(([ruleNameA], [ruleNameB]) => ruleNameA.localeCompare(ruleNameB)) - .map(([name, rule]) => [ - createRuleLink(name), - rule.meta.docs.description, - Boolean(rule.meta.fixable) ? emojiKey.fixable : '', - generateConfigBadges(rule.meta.docs.recommendedConfig), - ]); - -writeRulesList(rulesList); diff --git a/tools/generate-rules-list/utils.ts b/tools/generate-rules-list/utils.ts deleted file mode 100644 index 3f2f6051..00000000 --- a/tools/generate-rules-list/utils.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { readFileSync, writeFileSync } from 'fs'; -import { resolve } from 'path'; - -import { format, resolveConfig } from 'prettier'; - -import { - SUPPORTED_TESTING_FRAMEWORKS, - SupportedTestingFramework, -} from '../../lib/utils'; - -const prettierConfig = resolveConfig.sync(__dirname); -const readmePath = resolve(__dirname, `../../README.md`); - -export type RulesList = string[][]; - -export const configBadges = SUPPORTED_TESTING_FRAMEWORKS.reduce( - (badges, framework) => ({ - ...badges, - [framework]: `![${framework}-badge][]`, - }), - {} -) as Record; -export const emojiKey = { - fixable: 'πŸ”§', -} as const; -const staticElements = { - listHeaderRow: [ - 'Name', - 'Description', - emojiKey.fixable, - 'Included in configurations', - ], - listSpacerRow: Array(4).fill('-'), - rulesListKey: [ - `**Key**: ${emojiKey.fixable} = fixable`, - '', - [ - `**Configurations**:`, - Object.entries(configBadges) - .map(([template, badge]) => `${badge} = ${template}`) - .join(', '), - ].join(' '), - ].join('\n'), -}; - -const generateRulesListTable = (rulesList: RulesList) => - [staticElements.listHeaderRow, staticElements.listSpacerRow, ...rulesList] - .map((column) => `|${column.join('|')}|`) - .join('\n'); - -const generateRulesListMarkdown = (rulesList: RulesList) => - [ - '', - staticElements.rulesListKey, - '', - generateRulesListTable(rulesList), - '', - ].join('\n'); - -const listBeginMarker = ''; -const listEndMarker = ''; -const overWriteRulesList = (rulesList: RulesList, readme: string) => { - const listStartIndex = readme.indexOf(listBeginMarker); - const listEndIndex = readme.indexOf(listEndMarker); - - if ([listStartIndex, listEndIndex].includes(-1)) { - throw new Error(`cannot find start or end rules-list`); - } - - return [ - readme.substring(0, listStartIndex - 1), - listBeginMarker, - '', - generateRulesListMarkdown(rulesList), - readme.substring(listEndIndex), - ].join('\n'); -}; - -export const writeRulesList = (rulesList: RulesList): void => { - const readme = readFileSync(readmePath, 'utf8'); - const newReadme = format(overWriteRulesList(rulesList, readme), { - parser: 'markdown', - ...prettierConfig, - }); - - writeFileSync(readmePath, newReadme); -}; diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..f46aafbc --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["./tests/**/*.ts"] +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index ab3c206a..b0a6dfc7 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,4 +1,4 @@ { - "extends": "./tsconfig.json", - "include": ["**/*.ts", "**/*.js"] + "extends": "./tsconfig.json", + "include": ["**/*.ts", "**/*.js"] } diff --git a/tsconfig.json b/tsconfig.json index 863be8e7..c52787a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,20 @@ { - "compilerOptions": { - "strict": true, - "target": "es6", - "module": "commonjs", - "moduleResolution": "node", - "esModuleInterop": true, - "resolveJsonModule": true, - "forceConsistentCasingInFileNames": true, - "noImplicitAny": true, - "outDir": "./dist", - "removeComments": true, - "skipLibCheck": true, - "sourceMap": false, - "suppressImplicitAnyIndexErrors": true - }, - "include": ["./lib/**/*.ts"] + "compilerOptions": { + "strict": true, + "target": "ES2019", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2019"], + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "removeComments": true, + // TODO: turn it on + "noUncheckedIndexedAccess": false, + "outDir": "dist", + "sourceMap": false + }, + "include": ["./lib/**/*.ts", "./tests/**/*.ts"] }