From 341fb276f5402b2c147ce2f94ca119cfb21cb565 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Fri, 10 Oct 2025 14:50:38 -0400 Subject: [PATCH] Prevent committing tests with `.only`. Adds an eslint rule for mocha tests and suites marked with .only, including a version for our tests/suites wrapped with tag functionality. Adds eslint in to the precommit hook in addition to running the formatter. --- .eslintrc.json | 10 +++++- package-lock.json | 88 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 +++ test/tags.ts | 3 ++ 4 files changed, 105 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 76f5c51e1..b88534848 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,7 +6,7 @@ "sourceType": "module", "project": true }, - "plugins": ["@typescript-eslint"], + "plugins": ["@typescript-eslint", "mocha"], "rules": { "curly": "error", "eqeqeq": "warn", @@ -14,6 +14,14 @@ // TODO "@typescript-eslint/semi" rule moved to https://eslint.style "semi": "error", "no-console": "warn", + "mocha/no-exclusive-tests": "error", + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression[callee.object.object.callee.name='tag'][callee.property.name='only']", + "message": "Unexpected exclusive mocha test with tag().suite.only() or tag().test.only()" + } + ], "@typescript-eslint/no-floating-promises": ["warn", { "checkThenables": true }], "@typescript-eslint/await-thenable": "warn", // Mostly fails tests, ex. expect(...).to.be.true returns a Chai.Assertion diff --git a/package-lock.json b/package-lock.json index ef58c1cb4..4a7367f2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "esbuild": "^0.25.10", "eslint": "^8.57.0", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-mocha": "^10.5.0", "fantasticon": "^1.2.3", "husky": "^9.1.7", "lint-staged": "^16.2.3", @@ -5520,6 +5521,24 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-mocha": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", + "integrity": "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^3.0.0", + "globals": "^13.24.0", + "rambda": "^7.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -5536,6 +5555,34 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -9141,6 +9188,13 @@ } ] }, + "node_modules/rambda": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.5.0.tgz", + "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==", + "dev": true, + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -15575,6 +15629,17 @@ "dev": true, "requires": {} }, + "eslint-plugin-mocha": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", + "integrity": "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==", + "dev": true, + "requires": { + "eslint-utils": "^3.0.0", + "globals": "^13.24.0", + "rambda": "^7.4.0" + } + }, "eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -15585,6 +15650,23 @@ "estraverse": "^5.2.0" } }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, "eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -18183,6 +18265,12 @@ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, + "rambda": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.5.0.tgz", + "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", diff --git a/package.json b/package.json index f8bc125d3..57cb5735e 100644 --- a/package.json +++ b/package.json @@ -2009,6 +2009,10 @@ "prepare": "husky" }, "lint-staged": { + "**/*.ts": [ + "eslint --max-warnings=0", + "prettier --write" + ], "**/*": "prettier --write --ignore-unknown" }, "devDependencies": { @@ -2051,6 +2055,7 @@ "esbuild": "^0.25.10", "eslint": "^8.57.0", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-mocha": "^10.5.0", "fantasticon": "^1.2.3", "husky": "^9.1.7", "lint-staged": "^16.2.3", diff --git a/test/tags.ts b/test/tags.ts index c788b0b24..d047bd355 100644 --- a/test/tags.ts +++ b/test/tags.ts @@ -144,11 +144,13 @@ export function tag(size: TestSize): MochaFunctions { }; wrappedSuite.only = (title: string, fn?: (this: Suite) => void): Suite => { if (fn) { + // eslint-disable-next-line mocha/no-exclusive-tests return suite.only(title, function () { applyTags(this); fn.call(this); }); } + // eslint-disable-next-line mocha/no-exclusive-tests return suite.only(title); }; wrappedSuite.skip = (title: string, fn: (this: Suite) => void): Suite | void => { @@ -164,6 +166,7 @@ export function tag(size: TestSize): MochaFunctions { }; wrappedTest.only = (titleOrFn: string | AsyncFunc | Func, fn?: AsyncFunc | Func): Test => { return applyTags( + // eslint-disable-next-line mocha/no-exclusive-tests typeof titleOrFn === "string" ? test.only(titleOrFn, fn) : test.only(titleOrFn) ); };