eslint-node-test 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/configs/core-rule-replacements.js +9 -0
  2. package/configs/flat-config-base.js +9 -0
  3. package/index.d.ts +11 -0
  4. package/index.js +51 -0
  5. package/license +9 -0
  6. package/package.json +106 -0
  7. package/readme.md +143 -0
  8. package/rules/assertion-arguments.js +134 -0
  9. package/rules/ast/call-or-new-expression.js +100 -0
  10. package/rules/ast/function-types.js +7 -0
  11. package/rules/ast/index.js +17 -0
  12. package/rules/ast/is-expression-statement.js +7 -0
  13. package/rules/ast/is-function.js +5 -0
  14. package/rules/ast/is-loop.js +5 -0
  15. package/rules/ast/is-member-expression.js +98 -0
  16. package/rules/ast/is-method-call.js +62 -0
  17. package/rules/ast/literal.js +32 -0
  18. package/rules/ast/loop-types.js +9 -0
  19. package/rules/consistent-modifier-style.js +95 -0
  20. package/rules/consistent-test-context-name.js +75 -0
  21. package/rules/consistent-test-filename.js +70 -0
  22. package/rules/consistent-test-it.js +86 -0
  23. package/rules/fix/index.js +5 -0
  24. package/rules/fix/remove-argument.js +58 -0
  25. package/rules/fix/replace-member-expression-property.js +25 -0
  26. package/rules/hooks-order.js +132 -0
  27. package/rules/index.js +66 -0
  28. package/rules/max-assertions.js +87 -0
  29. package/rules/max-nested-describe.js +70 -0
  30. package/rules/no-assert-in-describe.js +51 -0
  31. package/rules/no-assert-in-hook.js +51 -0
  32. package/rules/no-assert-throws-async.js +114 -0
  33. package/rules/no-assert-throws-string.js +65 -0
  34. package/rules/no-async-describe.js +50 -0
  35. package/rules/no-async-fn-without-await.js +74 -0
  36. package/rules/no-callback-and-promise.js +56 -0
  37. package/rules/no-commented-tests.js +59 -0
  38. package/rules/no-conditional-assertion.js +101 -0
  39. package/rules/no-conditional-in-test.js +66 -0
  40. package/rules/no-conditional-tests.js +75 -0
  41. package/rules/no-conflicting-modifiers.js +73 -0
  42. package/rules/no-done-callback.js +58 -0
  43. package/rules/no-duplicate-hooks.js +75 -0
  44. package/rules/no-export.js +79 -0
  45. package/rules/no-identical-assertion-arguments.js +71 -0
  46. package/rules/no-identical-title.js +101 -0
  47. package/rules/no-incorrect-deep-equal.js +100 -0
  48. package/rules/no-incorrect-strict-equal.js +86 -0
  49. package/rules/no-loop-static-title.js +93 -0
  50. package/rules/no-misused-concurrency.js +85 -0
  51. package/rules/no-mock-timers-destructured-import.js +150 -0
  52. package/rules/no-nested-tests.js +71 -0
  53. package/rules/no-only-test.js +11 -0
  54. package/rules/no-skip-test.js +11 -0
  55. package/rules/no-skip-without-reason.js +88 -0
  56. package/rules/no-skip-without-return.js +127 -0
  57. package/rules/no-standalone-assert.js +51 -0
  58. package/rules/no-test-inside-hook.js +68 -0
  59. package/rules/no-test-return-statement.js +114 -0
  60. package/rules/no-todo-test.js +11 -0
  61. package/rules/no-unawaited-rejects.js +74 -0
  62. package/rules/no-unawaited-subtest.js +66 -0
  63. package/rules/no-unknown-test-options.js +77 -0
  64. package/rules/no-useless-assertion.js +47 -0
  65. package/rules/prefer-assert-match.js +245 -0
  66. package/rules/prefer-assert-throws.js +90 -0
  67. package/rules/prefer-async-await.js +203 -0
  68. package/rules/prefer-context-mock.js +59 -0
  69. package/rules/prefer-diagnostic.js +94 -0
  70. package/rules/prefer-equality-assertion.js +101 -0
  71. package/rules/prefer-hooks-on-top.js +73 -0
  72. package/rules/prefer-lowercase-title.js +119 -0
  73. package/rules/prefer-mock-method.js +115 -0
  74. package/rules/prefer-strict-assert.js +69 -0
  75. package/rules/prefer-test-context-assert.js +125 -0
  76. package/rules/prefer-todo.js +98 -0
  77. package/rules/require-assertion.js +92 -0
  78. package/rules/require-await-concurrent-subtests.js +119 -0
  79. package/rules/require-context-assert-with-plan.js +127 -0
  80. package/rules/require-hook.js +108 -0
  81. package/rules/require-throws-expectation.js +52 -0
  82. package/rules/require-top-level-describe.js +89 -0
  83. package/rules/rule/index.js +9 -0
  84. package/rules/rule/to-eslint-create.js +37 -0
  85. package/rules/rule/to-eslint-listener.js +40 -0
  86. package/rules/rule/to-eslint-problem.js +38 -0
  87. package/rules/rule/to-eslint-rule-fixer.js +49 -0
  88. package/rules/rule/to-eslint-rule.js +38 -0
  89. package/rules/rule/to-eslint-rules.js +10 -0
  90. package/rules/rule/unicorn-context.js +36 -0
  91. package/rules/rule/unicorn-listeners.js +65 -0
  92. package/rules/rule/utilities.js +26 -0
  93. package/rules/shared/test-modifier-rule.js +92 -0
  94. package/rules/test-title-format.js +86 -0
  95. package/rules/test-title.js +139 -0
  96. package/rules/utils/contains-suspension-point.js +35 -0
  97. package/rules/utils/escape-string.js +24 -0
  98. package/rules/utils/get-comments.js +15 -0
  99. package/rules/utils/get-documentation-url.js +9 -0
  100. package/rules/utils/get-enclosing-function.js +18 -0
  101. package/rules/utils/index.js +16 -0
  102. package/rules/utils/is-conditional-branch.js +37 -0
  103. package/rules/utils/is-promise-type.js +28 -0
  104. package/rules/utils/is-same-reference.js +179 -0
  105. package/rules/utils/is-value-not-usable.js +5 -0
  106. package/rules/utils/node-test.js +713 -0
  107. package/rules/utils/parentheses/get-parent-syntax-opening-parenthesis.js +80 -0
  108. package/rules/utils/parentheses/iterate-surrounding-parentheses.js +82 -0
  109. package/rules/utils/parentheses/parentheses.js +69 -0
  110. package/rules/utils/types.js +5 -0
  111. package/rules/utils/unwrap-typescript-expression.js +16 -0
  112. package/rules/valid-describe-callback.js +63 -0
@@ -0,0 +1,9 @@
1
+ const coreRuleReplacements = [
2
+ 'id-match',
3
+ 'logical-assignment-operators',
4
+ 'no-negated-condition',
5
+ 'no-nested-ternary',
6
+ 'no-process-exit',
7
+ ];
8
+
9
+ export default coreRuleReplacements;
@@ -0,0 +1,9 @@
1
+ import globals from 'globals';
2
+
3
+ const config = {
4
+ languageOptions: {
5
+ globals: globals.builtin,
6
+ },
7
+ };
8
+
9
+ export default config;
package/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type {ESLint, Linter} from 'eslint';
2
+
3
+ declare const eslintNodeTest: ESLint.Plugin & {
4
+ configs: {
5
+ recommended: Linter.Config;
6
+ unopinionated: Linter.Config;
7
+ all: Linter.Config;
8
+ };
9
+ };
10
+
11
+ export default eslintNodeTest;
package/index.js ADDED
@@ -0,0 +1,51 @@
1
+ import flatConfigBase from './configs/flat-config-base.js';
2
+ import * as rawRules from './rules/index.js';
3
+ import {toEslintRules} from './rules/rule/index.js';
4
+ import packageJson from './package.json' with {type: 'json'};
5
+
6
+ const rules = toEslintRules(rawRules);
7
+
8
+ const recommendedRules = Object.fromEntries(Object.entries(rules).map(([id, rule]) => [
9
+ `node-test/${id}`,
10
+ rule.meta.docs.recommended ? 'error' : 'off',
11
+ ]));
12
+
13
+ const unopinionatedRules = Object.fromEntries(Object.entries(rules).map(([id, rule]) => [
14
+ `node-test/${id}`,
15
+ rule.meta.docs.recommended === 'unopinionated' ? 'error' : 'off',
16
+ ]));
17
+
18
+ const allRules = Object.fromEntries(Object.keys(rules).map(id => [
19
+ `node-test/${id}`,
20
+ 'error',
21
+ ]));
22
+
23
+ const createConfig = (rules, flatConfigName) => ({
24
+ ...flatConfigBase,
25
+ name: flatConfigName,
26
+ plugins: {
27
+ 'node-test': nodeTest,
28
+ },
29
+ rules,
30
+ });
31
+
32
+ const nodeTest = {
33
+ meta: {
34
+ // `eslint-doc-generator` derives the rule prefix from this name, expecting either the
35
+ // `eslint-plugin-<prefix>` convention or the prefix itself; our package name doesn't
36
+ // follow that convention, so use the prefix directly to keep doc generation working.
37
+ name: 'node-test',
38
+ version: packageJson.version,
39
+ },
40
+ rules,
41
+ };
42
+
43
+ const configs = {
44
+ recommended: createConfig(recommendedRules, 'node-test/recommended'),
45
+ unopinionated: createConfig(unopinionatedRules, 'node-test/unopinionated'),
46
+ all: createConfig(allRules, 'node-test/all'),
47
+ };
48
+
49
+ nodeTest.configs = configs;
50
+
51
+ export default nodeTest;
package/license ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "eslint-node-test",
3
+ "version": "0.0.1",
4
+ "description": "ESLint rules for the Node.js built-in test runner (`node:test`)",
5
+ "license": "MIT",
6
+ "repository": "sindresorhus/eslint-node-test",
7
+ "funding": "https://github.com/sindresorhus/eslint-node-test?sponsor=1",
8
+ "author": {
9
+ "name": "Sindre Sorhus",
10
+ "email": "sindresorhus@gmail.com",
11
+ "url": "https://sindresorhus.com"
12
+ },
13
+ "type": "module",
14
+ "exports": {
15
+ "types": "./index.d.ts",
16
+ "default": "./index.js"
17
+ },
18
+ "sideEffects": false,
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "scripts": {
23
+ "coverage": "node --test --experimental-test-coverage test/*.js test/unit/*.js",
24
+ "create-rule": "node ./scripts/create-rule.js && npm run create-rules-index-file && npm run fix:eslint-docs",
25
+ "create-rules-index-file": "node ./scripts/create-rules-index-file.js",
26
+ "fix": "run-p --continue-on-error \"fix:*\"",
27
+ "fix:eslint-docs": "eslint-doc-generator",
28
+ "fix:js": "npm run lint:js -- --fix",
29
+ "fix:markdown": "npm run lint:markdown -- --fix",
30
+ "fix:snapshots": "node --test --test-update-snapshots test/*.js test/unit/*.js",
31
+ "lint": "run-p --continue-on-error \"lint:*\"",
32
+ "lint:dogfooding": "eslint --config=./eslint.dogfooding.config.js",
33
+ "lint:eslint-docs": "npm run fix:eslint-docs -- --check",
34
+ "lint:js": "eslint",
35
+ "lint:markdown": "markdownlint \"**/*.md\"",
36
+ "lint:package-json": "npmPkgJsonLint .",
37
+ "rename-rule": "node ./scripts/rename-rule.js && npm run create-rules-index-file && npm run fix:eslint-docs",
38
+ "run-rules-on-codebase": "npm run lint:dogfooding",
39
+ "test": "npm-run-all --node-run --continue-on-error lint \"test:*\"",
40
+ "test:js": "node --test test/*.js test/unit/*.js"
41
+ },
42
+ "files": [
43
+ "index.js",
44
+ "index.d.ts",
45
+ "rules",
46
+ "configs"
47
+ ],
48
+ "keywords": [
49
+ "eslint",
50
+ "eslintplugin",
51
+ "eslint-plugin",
52
+ "node-test",
53
+ "node:test",
54
+ "test-runner",
55
+ "testing",
56
+ "test",
57
+ "linter",
58
+ "lint",
59
+ "style",
60
+ "xo"
61
+ ],
62
+ "dependencies": {
63
+ "@babel/helper-validator-identifier": "^7.29.7",
64
+ "@eslint-community/eslint-utils": "^4.9.1",
65
+ "globals": "^17.6.0",
66
+ "jsesc": "^3.1.0",
67
+ "pluralize": "^8.0.0"
68
+ },
69
+ "devDependencies": {
70
+ "@babel/code-frame": "^8.0.0-rc.6",
71
+ "@eslint/css": "^1.3.0",
72
+ "@eslint/eslintrc": "^3.3.5",
73
+ "@eslint/json": "^2.0.0",
74
+ "@eslint/markdown": "^8.0.2",
75
+ "@html-eslint/eslint-plugin": "^0.62.0",
76
+ "@html-eslint/parser": "^0.62.0",
77
+ "@lubien/fixture-beta-package": "^1.0.0-beta.1",
78
+ "@typescript-eslint/parser": "^8.61.0",
79
+ "@typescript-eslint/types": "^8.61.0",
80
+ "enquirer": "^2.4.1",
81
+ "eslint": "^10.4.1",
82
+ "eslint-config-xo": "^0.52.0",
83
+ "eslint-doc-generator": "^3.6.0",
84
+ "eslint-plugin-eslint-plugin": "^7.4.0",
85
+ "eslint-plugin-jsdoc": "^63.0.2",
86
+ "eslint-plugin-unicorn": "^65.0.1",
87
+ "eslint-scope": "^9.1.2",
88
+ "espree": "^11.2.0",
89
+ "indent-string": "^5.0.0",
90
+ "listr2": "^10.2.1",
91
+ "markdownlint-cli": "^0.48.0",
92
+ "nano-spawn": "^2.1.0",
93
+ "node-style-text": "^2.1.2",
94
+ "npm-package-json-lint": "^10.4.0",
95
+ "npm-run-all2": "^9.0.1",
96
+ "open-editor": "^6.0.0",
97
+ "outdent": "^0.8.0",
98
+ "pretty-ms": "^9.3.0",
99
+ "typescript": "^6.0.3",
100
+ "vue-eslint-parser": "^10.4.1",
101
+ "yaml": "^2.9.0"
102
+ },
103
+ "peerDependencies": {
104
+ "eslint": ">=10.4"
105
+ }
106
+ }
package/readme.md ADDED
@@ -0,0 +1,143 @@
1
+ # eslint-node-test
2
+
3
+ > ESLint rules for the [Node.js built-in test runner](https://nodejs.org/api/test.html) (`node:test`)
4
+
5
+ This plugin helps you avoid common mistakes and write more consistent tests with `node:test`, the test runner built into Node.js.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install --save-dev eslint eslint-node-test
11
+ ```
12
+
13
+ **Requires ESLint `>=10.4`, [flat config](https://eslint.org/docs/latest/use/configure/configuration-files), and [ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-make-my-typescript-project-output-esm).**
14
+
15
+ ## Usage
16
+
17
+ Use a [preset config](#preset-configs) or configure each rule in `eslint.config.js`.
18
+
19
+ ```js
20
+ import eslintNodeTest from 'eslint-node-test';
21
+ import {defineConfig} from 'eslint/config';
22
+
23
+ export default defineConfig([
24
+ eslintNodeTest.configs.recommended,
25
+ ]);
26
+ ```
27
+
28
+ Or configure rules individually:
29
+
30
+ ```js
31
+ import eslintNodeTest from 'eslint-node-test';
32
+ import {defineConfig} from 'eslint/config';
33
+
34
+ export default defineConfig([
35
+ {
36
+ plugins: {
37
+ 'node-test': eslintNodeTest,
38
+ },
39
+ rules: {
40
+ 'node-test/no-only-test': 'error',
41
+ 'node-test/no-identical-title': 'error',
42
+ },
43
+ },
44
+ ]);
45
+ ```
46
+
47
+ The rules only activate in files that import from `node:test` (and, for assertion rules, `node:assert`), so you can safely apply the plugin across your whole project.
48
+
49
+ ## Rules
50
+
51
+ <!-- begin auto-generated rules list -->
52
+
53
+ 💼 [Configurations](https://github.com/sindresorhus/eslint-node-test#preset-configs) enabled in.\
54
+ ✅ Set in the `recommended` [configuration](https://github.com/sindresorhus/eslint-node-test#preset-configs).\
55
+ ☑️ Set in the `unopinionated` [configuration](https://github.com/sindresorhus/eslint-node-test#preset-configs).\
56
+ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
57
+ 💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
58
+
59
+ | Name                               | Description | 💼 | 🔧 | 💡 |
60
+ | :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :--- | :- | :- |
61
+ | [assertion-arguments](docs/rules/assertion-arguments.md) | Enforce the correct number of arguments for `node:assert` assertions. | ✅ ☑️ | | |
62
+ | [consistent-modifier-style](docs/rules/consistent-modifier-style.md) | Enforce a consistent style for test modifiers. | | | |
63
+ | [consistent-test-context-name](docs/rules/consistent-test-context-name.md) | Enforce a consistent name for the test context parameter. | ✅ ☑️ | | |
64
+ | [consistent-test-filename](docs/rules/consistent-test-filename.md) | Enforce a consistent test file name pattern. | | | |
65
+ | [consistent-test-it](docs/rules/consistent-test-it.md) | Enforce consistent use of `test` or `it`. | | | |
66
+ | [hooks-order](docs/rules/hooks-order.md) | Enforce a consistent order of hook declarations. | ✅ ☑️ | 🔧 | |
67
+ | [max-assertions](docs/rules/max-assertions.md) | Enforce a maximum number of assertions in a test. | | | |
68
+ | [max-nested-describe](docs/rules/max-nested-describe.md) | Enforce a maximum depth for nested `describe` blocks. | ✅ | | |
69
+ | [no-assert-in-describe](docs/rules/no-assert-in-describe.md) | Disallow assertions directly inside a `describe` body. | ✅ | | |
70
+ | [no-assert-in-hook](docs/rules/no-assert-in-hook.md) | Disallow assertions inside hooks. | | | |
71
+ | [no-assert-throws-async](docs/rules/no-assert-throws-async.md) | Disallow passing an async function to `assert.throws()`/`assert.doesNotThrow()`. | ✅ ☑️ | | 💡 |
72
+ | [no-assert-throws-string](docs/rules/no-assert-throws-string.md) | Disallow a string as the error matcher of `assert.throws()`/`assert.rejects()`. | ✅ ☑️ | | 💡 |
73
+ | [no-async-describe](docs/rules/no-async-describe.md) | Disallow `async` `describe` callbacks. | ✅ ☑️ | | |
74
+ | [no-async-fn-without-await](docs/rules/no-async-fn-without-await.md) | Disallow async test/hook functions that have no `await` expression. | ✅ ☑️ | | 💡 |
75
+ | [no-callback-and-promise](docs/rules/no-callback-and-promise.md) | Disallow a test or hook from using both a callback and a Promise. | ✅ ☑️ | | |
76
+ | [no-commented-tests](docs/rules/no-commented-tests.md) | Disallow commented-out tests. | | | |
77
+ | [no-conditional-assertion](docs/rules/no-conditional-assertion.md) | Disallow assertions inside conditional code within a test. | ✅ ☑️ | | |
78
+ | [no-conditional-in-test](docs/rules/no-conditional-in-test.md) | Disallow conditional logic inside tests. | | | |
79
+ | [no-conditional-tests](docs/rules/no-conditional-tests.md) | Disallow conditionally registering tests and suites. | ✅ | | |
80
+ | [no-conflicting-modifiers](docs/rules/no-conflicting-modifiers.md) | Disallow conflicting `only`/`skip`/`todo` modifiers. | ✅ ☑️ | | |
81
+ | [no-done-callback](docs/rules/no-done-callback.md) | Disallow callback (`done`) parameters in tests and hooks. | | | |
82
+ | [no-duplicate-hooks](docs/rules/no-duplicate-hooks.md) | Disallow duplicate hooks within the same scope. | ✅ | | |
83
+ | [no-export](docs/rules/no-export.md) | Disallow exports from test files. | ✅ | | |
84
+ | [no-identical-assertion-arguments](docs/rules/no-identical-assertion-arguments.md) | Disallow comparing a value to itself in an assertion. | ✅ ☑️ | | |
85
+ | [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical test titles within the same scope. | ✅ ☑️ | | |
86
+ | [no-incorrect-deep-equal](docs/rules/no-incorrect-deep-equal.md) | Disallow `deepEqual`/`deepStrictEqual` (and their `notDeep*` variants) when comparing with primitives. | ✅ ☑️ | 🔧 | |
87
+ | [no-incorrect-strict-equal](docs/rules/no-incorrect-strict-equal.md) | Disallow `strictEqual`/`equal` (and their `not*` variants) when comparing with an object or array literal. | ✅ ☑️ | 🔧 | |
88
+ | [no-loop-static-title](docs/rules/no-loop-static-title.md) | Disallow a static test or suite title inside a loop. | ✅ ☑️ | | |
89
+ | [no-misused-concurrency](docs/rules/no-misused-concurrency.md) | Disallow the `concurrency` option on a test without subtests. | ✅ ☑️ | | |
90
+ | [no-mock-timers-destructured-import](docs/rules/no-mock-timers-destructured-import.md) | Disallow destructured timer imports when using `mock.timers`. | ✅ ☑️ | | |
91
+ | [no-nested-tests](docs/rules/no-nested-tests.md) | Disallow tests and suites nested inside a test body. | ✅ ☑️ | | |
92
+ | [no-only-test](docs/rules/no-only-test.md) | Disallow the `.only` test modifier. | ✅ ☑️ | | 💡 |
93
+ | [no-skip-test](docs/rules/no-skip-test.md) | Disallow the `.skip` test modifier. | ✅ | | 💡 |
94
+ | [no-skip-without-reason](docs/rules/no-skip-without-reason.md) | Require a reason when skipping or marking a test as todo. | | | |
95
+ | [no-skip-without-return](docs/rules/no-skip-without-return.md) | Disallow `t.skip()`/`t.todo()` without returning afterwards. | ✅ | | 💡 |
96
+ | [no-standalone-assert](docs/rules/no-standalone-assert.md) | Disallow assertions outside of a test. | ✅ ☑️ | | |
97
+ | [no-test-inside-hook](docs/rules/no-test-inside-hook.md) | Disallow defining tests and suites inside a hook. | ✅ ☑️ | | |
98
+ | [no-test-return-statement](docs/rules/no-test-return-statement.md) | Disallow returning a non-Promise value from a test. | ✅ | | |
99
+ | [no-todo-test](docs/rules/no-todo-test.md) | Disallow the `.todo` test modifier. | | | 💡 |
100
+ | [no-unawaited-rejects](docs/rules/no-unawaited-rejects.md) | Require `assert.rejects()`/`assert.doesNotReject()` to be awaited or returned. | ✅ ☑️ | 🔧 | |
101
+ | [no-unawaited-subtest](docs/rules/no-unawaited-subtest.md) | Require subtests created with the test context to be awaited or returned. | ✅ | 🔧 | |
102
+ | [no-unknown-test-options](docs/rules/no-unknown-test-options.md) | Disallow unknown options in test and hook option objects. | ✅ | | |
103
+ | [no-useless-assertion](docs/rules/no-useless-assertion.md) | Disallow `assert.doesNotThrow()` and `assert.doesNotReject()`. | ✅ | | |
104
+ | [prefer-assert-match](docs/rules/prefer-assert-match.md) | Prefer `assert.match()`/`assert.doesNotMatch()` over asserting `RegExp#test()` / `String#match()` results. | ✅ ☑️ | 🔧 | |
105
+ | [prefer-assert-throws](docs/rules/prefer-assert-throws.md) | Prefer `assert.throws()`/`assert.rejects()` over try/catch with an assertion. | ✅ | | |
106
+ | [prefer-async-await](docs/rules/prefer-async-await.md) | Prefer async/await over returning a Promise. | ✅ | | |
107
+ | [prefer-context-mock](docs/rules/prefer-context-mock.md) | Prefer the test context `t.mock` over the global `mock`. | ✅ | | |
108
+ | [prefer-diagnostic](docs/rules/prefer-diagnostic.md) | Prefer the test context `diagnostic()` over `console` inside tests. | | | 💡 |
109
+ | [prefer-equality-assertion](docs/rules/prefer-equality-assertion.md) | Prefer an equality assertion over a truthiness assertion on a comparison. | ✅ ☑️ | 🔧 | |
110
+ | [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Require hooks to be declared before the tests in their scope. | ✅ | | |
111
+ | [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test titles. | | 🔧 | |
112
+ | [prefer-mock-method](docs/rules/prefer-mock-method.md) | Prefer `mock.method()` over assigning `mock.fn()` to an object property. | ✅ | | 💡 |
113
+ | [prefer-strict-assert](docs/rules/prefer-strict-assert.md) | Prefer strict assertion methods over their legacy loose counterparts. | ✅ ☑️ | 🔧 | |
114
+ | [prefer-test-context-assert](docs/rules/prefer-test-context-assert.md) | Prefer the test context `t.assert` over the imported `node:assert`. | ✅ | | 💡 |
115
+ | [prefer-todo](docs/rules/prefer-todo.md) | Prefer `.todo` for empty placeholder tests. | ✅ | | 💡 |
116
+ | [require-assertion](docs/rules/require-assertion.md) | Require that each test contains at least one assertion. | ✅ | | |
117
+ | [require-await-concurrent-subtests](docs/rules/require-await-concurrent-subtests.md) | Require subtests created in a loop callback to be awaited. | ✅ ☑️ | | |
118
+ | [require-context-assert-with-plan](docs/rules/require-context-assert-with-plan.md) | Require assertions to use the test context when the test sets a plan. | ✅ ☑️ | | |
119
+ | [require-hook](docs/rules/require-hook.md) | Require setup and teardown code to be inside a hook. | | | |
120
+ | [require-throws-expectation](docs/rules/require-throws-expectation.md) | Require an error matcher for `assert.throws()`/`assert.rejects()`. | ✅ ☑️ | | |
121
+ | [require-top-level-describe](docs/rules/require-top-level-describe.md) | Require tests and hooks to be inside a top-level `describe`. | | | |
122
+ | [test-title](docs/rules/test-title.md) | Require tests to have a title. | ✅ | 🔧 | |
123
+ | [test-title-format](docs/rules/test-title-format.md) | Require test titles to match a configured pattern. | | | |
124
+ | [valid-describe-callback](docs/rules/valid-describe-callback.md) | Enforce valid `describe` callbacks. | ✅ ☑️ | | |
125
+
126
+ <!-- end auto-generated rules list -->
127
+
128
+ ## Preset configs
129
+
130
+ This plugin exports these configs:
131
+
132
+ - `recommended` — Enables the recommended rules.
133
+ - `unopinionated` — A subset of `recommended` with only the most uncontroversial rules.
134
+ - `all` — Enables every rule (not recommended for general use; useful to discover new rules).
135
+
136
+ ```js
137
+ import eslintNodeTest from 'eslint-node-test';
138
+ import {defineConfig} from 'eslint/config';
139
+
140
+ export default defineConfig([
141
+ eslintNodeTest.configs.recommended,
142
+ ]);
143
+ ```
@@ -0,0 +1,134 @@
1
+ import {resolveImports, parseAssertionCall} from './utils/node-test.js';
2
+ import {isFunction} from './ast/index.js';
3
+ import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
4
+
5
+ const MESSAGE_ID_TOO_FEW = 'too-few-arguments';
6
+ const MESSAGE_ID_TOO_MANY = 'too-many-arguments';
7
+ const MESSAGE_ID_NOT_STRING = 'not-string-message';
8
+
9
+ /*
10
+ Map of node:assert method -> required argument count.
11
+ Each method also accepts one optional trailing `message` argument, making max = required + 1.
12
+ `fail` is omitted because it accepts 0 or 1 args (ambiguous) — not checkable.
13
+ `throws`/`doesNotThrow`/`rejects`/`doesNotReject` accept 1 required + optional error + optional message (max 3).
14
+ `ifError` is the exception with no trailing message argument — it accepts exactly one value (max = min = 1).
15
+ `snapshot` is omitted because its optional second argument is an options object, not a message string,
16
+ so it does not fit this map's "trailing string message" model (and it is a `node:test` context
17
+ assertion rather than a `node:assert` method).
18
+ */
19
+ const ASSERTION_ARGS = new Map([
20
+ ['ok', {min: 1, max: 2}],
21
+ ['equal', {min: 2, max: 3}],
22
+ ['notEqual', {min: 2, max: 3}],
23
+ ['strictEqual', {min: 2, max: 3}],
24
+ ['notStrictEqual', {min: 2, max: 3}],
25
+ ['deepEqual', {min: 2, max: 3}],
26
+ ['notDeepEqual', {min: 2, max: 3}],
27
+ ['deepStrictEqual', {min: 2, max: 3}],
28
+ ['notDeepStrictEqual', {min: 2, max: 3}],
29
+ ['match', {min: 2, max: 3}],
30
+ ['doesNotMatch', {min: 2, max: 3}],
31
+ ['throws', {min: 1, max: 3}],
32
+ ['doesNotThrow', {min: 1, max: 3}],
33
+ ['rejects', {min: 1, max: 3}],
34
+ ['doesNotReject', {min: 1, max: 3}],
35
+ ['ifError', {min: 1, max: 1}],
36
+ ]);
37
+
38
+ /*
39
+ The optional trailing `message` argument accepts a string or an `Error`. Only flag values that
40
+ are statically known to be neither: object/array/function literals, or non-string literals
41
+ (numbers, booleans, `null`, regexes). Identifiers, calls, member expressions, template
42
+ literals, conditionals, logical/binary expressions, and TypeScript casts can all resolve to a
43
+ string or `Error` at runtime, so they are left alone to avoid false positives.
44
+ */
45
+ function isInvalidMessageArgument(node) {
46
+ node = unwrapTypeScriptExpression(node);
47
+
48
+ if (node.type === 'ArrayExpression' || node.type === 'ObjectExpression' || isFunction(node)) {
49
+ return true;
50
+ }
51
+
52
+ return node.type === 'Literal' && typeof node.value !== 'string';
53
+ }
54
+
55
+ /** @param {import('eslint').Rule.RuleContext} context */
56
+ const create = context => {
57
+ const imports = resolveImports(context);
58
+ if (!imports.isAssertOrTestFile) {
59
+ return;
60
+ }
61
+
62
+ context.on('CallExpression', node => {
63
+ const assertion = parseAssertionCall(node, imports);
64
+ if (!assertion) {
65
+ return;
66
+ }
67
+
68
+ const {method} = assertion;
69
+ const expected = ASSERTION_ARGS.get(method);
70
+ if (!expected) {
71
+ // Unknown method or `fail` — skip.
72
+ return;
73
+ }
74
+
75
+ // Skip calls with spread arguments — arg count is not statically known.
76
+ if (node.arguments.some(argument => argument.type === 'SpreadElement')) {
77
+ return;
78
+ }
79
+
80
+ const {min, max} = expected;
81
+ const count = node.arguments.length;
82
+
83
+ if (count < min) {
84
+ return {
85
+ node,
86
+ messageId: MESSAGE_ID_TOO_FEW,
87
+ data: {min},
88
+ };
89
+ }
90
+
91
+ if (count > max) {
92
+ return {
93
+ node,
94
+ messageId: MESSAGE_ID_TOO_MANY,
95
+ data: {max},
96
+ };
97
+ }
98
+
99
+ // If a trailing message argument is present, it must be a string.
100
+ // The message argument is the last arg when count > min (i.e. it is optional and present).
101
+ // For methods where max === min there is no message slot — skip.
102
+ if (count === max && max > min) {
103
+ const lastArg = node.arguments.at(-1);
104
+ if (isInvalidMessageArgument(lastArg)) {
105
+ return {
106
+ node: lastArg,
107
+ messageId: MESSAGE_ID_NOT_STRING,
108
+ };
109
+ }
110
+ }
111
+ });
112
+ };
113
+
114
+ /** @type {import('eslint').Rule.RuleModule} */
115
+ const config = {
116
+ create,
117
+ meta: {
118
+ type: 'problem',
119
+ docs: {
120
+ description: 'Enforce the correct number of arguments for `node:assert` assertions.',
121
+ recommended: 'unopinionated',
122
+ },
123
+ fixable: undefined,
124
+ schema: [],
125
+ messages: {
126
+ [MESSAGE_ID_TOO_FEW]: 'Not enough arguments. Expected at least {{min}}.',
127
+ [MESSAGE_ID_TOO_MANY]: 'Too many arguments. Expected at most {{max}}.',
128
+ [MESSAGE_ID_NOT_STRING]: 'Assertion message must be a string.',
129
+ },
130
+ languages: ['js/js'],
131
+ },
132
+ };
133
+
134
+ export default config;
@@ -0,0 +1,100 @@
1
+ /**
2
+ @typedef {
3
+ {
4
+ name?: string,
5
+ names?: string[],
6
+ argumentsLength?: number,
7
+ minimumArguments?: number,
8
+ maximumArguments?: number,
9
+ allowSpreadElement?: boolean,
10
+ optional?: boolean,
11
+ } | string | string[]
12
+ } CallOrNewExpressionCheckOptions
13
+ */
14
+ // eslint-disable-next-line complexity
15
+ function create(node, options, types) {
16
+ if (!types.includes(node?.type)) {
17
+ return false;
18
+ }
19
+
20
+ if (typeof options === 'string') {
21
+ options = {names: [options]};
22
+ }
23
+
24
+ if (Array.isArray(options)) {
25
+ options = {names: options};
26
+ }
27
+
28
+ let {
29
+ name,
30
+ names,
31
+ argumentsLength,
32
+ minimumArguments,
33
+ maximumArguments,
34
+ allowSpreadElement,
35
+ optional,
36
+ } = {
37
+ minimumArguments: 0,
38
+ maximumArguments: Infinity,
39
+ allowSpreadElement: false,
40
+ ...options,
41
+ };
42
+
43
+ if (name) {
44
+ names = [name];
45
+ }
46
+
47
+ if (
48
+ (optional === true && (node.optional !== optional))
49
+ || (
50
+ optional === false
51
+ // `node.optional` can be `undefined` in some parsers
52
+ && node.optional
53
+ )
54
+ ) {
55
+ return false;
56
+ }
57
+
58
+ if (typeof argumentsLength === 'number' && node.arguments.length !== argumentsLength) {
59
+ return false;
60
+ }
61
+
62
+ if (minimumArguments !== 0 && node.arguments.length < minimumArguments) {
63
+ return false;
64
+ }
65
+
66
+ if (Number.isFinite(maximumArguments) && node.arguments.length > maximumArguments) {
67
+ return false;
68
+ }
69
+
70
+ if (!allowSpreadElement) {
71
+ const maximumArgumentsLength = Number.isFinite(maximumArguments) ? maximumArguments : argumentsLength;
72
+ if (
73
+ typeof maximumArgumentsLength === 'number'
74
+ && node.arguments.some((node, index) =>
75
+ node.type === 'SpreadElement'
76
+ && index < maximumArgumentsLength)
77
+ ) {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ if (
83
+ Array.isArray(names)
84
+ && names.length > 0
85
+ && (
86
+ node.callee.type !== 'Identifier'
87
+ || !names.includes(node.callee.name)
88
+ )
89
+ ) {
90
+ return false;
91
+ }
92
+
93
+ return true;
94
+ }
95
+
96
+ /**
97
+ @param {CallOrNewExpressionCheckOptions} [options]
98
+ @returns {boolean}
99
+ */
100
+ export const isCallExpression = (node, options) => create(node, options, ['CallExpression']);
@@ -0,0 +1,7 @@
1
+ const functionTypes = [
2
+ 'FunctionDeclaration',
3
+ 'FunctionExpression',
4
+ 'ArrowFunctionExpression',
5
+ ];
6
+
7
+ export default functionTypes;
@@ -0,0 +1,17 @@
1
+ export {
2
+ isLiteral,
3
+ isStringExpression,
4
+ isBooleanLiteral,
5
+ getStaticStringValue,
6
+ isRegexLiteral,
7
+ } from './literal.js';
8
+
9
+ export {isCallExpression} from './call-or-new-expression.js';
10
+
11
+ export {default as isExpressionStatement} from './is-expression-statement.js';
12
+ export {default as isFunction} from './is-function.js';
13
+ export {default as isLoop} from './is-loop.js';
14
+ export {default as isMemberExpression} from './is-member-expression.js';
15
+ export {default as isMethodCall} from './is-method-call.js';
16
+ export {default as functionTypes} from './function-types.js';
17
+ export {default as loopTypes} from './loop-types.js';
@@ -0,0 +1,7 @@
1
+ export default function isExpressionStatement(node) {
2
+ return node.type === 'ExpressionStatement'
3
+ || (
4
+ node.type === 'ChainExpression'
5
+ && node.parent.type === 'ExpressionStatement'
6
+ );
7
+ }
@@ -0,0 +1,5 @@
1
+ import functionTypes from './function-types.js';
2
+
3
+ export default function isFunction(node) {
4
+ return functionTypes.includes(node.type);
5
+ }
@@ -0,0 +1,5 @@
1
+ import loopTypes from './loop-types.js';
2
+
3
+ export default function isLoop(node) {
4
+ return loopTypes.includes(node.type);
5
+ }