From 1a8c2b3a280f9430d2a0b1cc3d3d7ad56389420d Mon Sep 17 00:00:00 2001 From: Harshal Devmurari <92778686+harshal015@users.noreply.github.com> Date: Mon, 25 May 2026 12:37:03 +0530 Subject: [PATCH] add new rule --- packages/core/src/index.js | 2 + .../rules/require-test-assignee-comment.js | 67 +++++++++++++++++++ .../require-test-assignee-comment.test.js | 39 +++++++++++ .../rules/require-test-assignee-comment.md | 52 ++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 packages/core/src/rules/require-test-assignee-comment.js create mode 100644 packages/core/src/tests/require-test-assignee-comment.test.js create mode 100644 packages/docs/rules/require-test-assignee-comment.md diff --git a/packages/core/src/index.js b/packages/core/src/index.js index b629ac0..d9b666c 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -4,6 +4,7 @@ import noDatepipeTransformFormatArg from './rules/no-datepipe-transform-format-a import noInlineDateFormat from './rules/no-inline-date-format.js'; import noHardcodedStrings from './rules/no-hardcoded-strings.js'; import noDateCurrencyMutation from './rules/no-date-currency-mutation.js'; +import requireTestAssigneeComment from './rules/require-test-assignee-comment.js'; const rules = { 'i18n-key-naming-convention': i18nKeyNamingConvention, @@ -12,6 +13,7 @@ const rules = { 'no-inline-date-format': noInlineDateFormat, 'no-hardcoded-strings': noHardcodedStrings, 'no-date-currency-mutation': noDateCurrencyMutation, + 'require-test-assignee-comment': requireTestAssigneeComment, }; const configs = { diff --git a/packages/core/src/rules/require-test-assignee-comment.js b/packages/core/src/rules/require-test-assignee-comment.js new file mode 100644 index 0000000..470dfa6 --- /dev/null +++ b/packages/core/src/rules/require-test-assignee-comment.js @@ -0,0 +1,67 @@ +import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils'; + +const createRule = ESLintUtils.RuleCreator( + () => 'https://github.com/fylein/fyle-eslint-plugin/blob/main/packages/docs/rules/require-test-assignee-comment.md', +); + +const RULE_NAME = 'require-test-assignee-comment'; + +const ASSIGNEE_COMMENT_PATTERN = /Assignee:\s+@[\w.-]+/i; + +export default createRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Require an explicit assignee comment for Playwright `test.fixme` and `test.fail` cases.', + recommended: 'recommended', + }, + schema: [], + messages: { + missingAssignee: + 'test.fixme/test.fail must include an explicit assignee comment immediately above the call, e.g. "// @assignee @username".', + }, + }, + defaultOptions: [], + create(context) { + const sourceCode = context.getSourceCode(); + + function isTestFixmeOrFailCall(node) { + if (node.type !== AST_NODE_TYPES.CallExpression) { + return false; + } + + const callee = node.callee; + if (callee.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + + const object = callee.object; + const property = callee.property; + if (object.type !== AST_NODE_TYPES.Identifier || object.name !== 'test') { + return false; + } + + return property.type === AST_NODE_TYPES.Identifier && (property.name === 'fixme' || property.name === 'fail'); + } + + function hasAssigneeComment(node) { + const statement = node.parent && node.parent.type === AST_NODE_TYPES.ExpressionStatement ? node.parent : node; + const leadingComments = sourceCode.getCommentsBefore(statement); + + return leadingComments.some((comment) => ASSIGNEE_COMMENT_PATTERN.test(comment.value)); + } + + return { + CallExpression(node) { + if (!isTestFixmeOrFailCall(node)) { + return; + } + + if (!hasAssigneeComment(node)) { + context.report({ node, messageId: 'missingAssignee' }); + } + }, + }; + }, +}); diff --git a/packages/core/src/tests/require-test-assignee-comment.test.js b/packages/core/src/tests/require-test-assignee-comment.test.js new file mode 100644 index 0000000..e442fc4 --- /dev/null +++ b/packages/core/src/tests/require-test-assignee-comment.test.js @@ -0,0 +1,39 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; +import tsParser from '@typescript-eslint/parser'; +import rule from '../rules/require-test-assignee-comment.js'; + +const ruleTester = new RuleTester({ + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + }, +}); + +ruleTester.run('require-test-assignee-comment', rule, { + valid: [ + `// Assignee: @arjun\n test.fixme('known issue', async ({ page }) => {});`, + `test('works normally', async ({ page }) => {});`, + `test.skip('skipped test without fail', async ({ page }) => {});`, + ], + invalid: [ + { + code: `test.fixme('known issue', async ({ page }) => {});`, + errors: [{ messageId: 'missingAssignee' }], + }, + { + code: `// TODO: temporary bug\n test.fail('unstable feature', async ({ page }) => {});`, + errors: [{ messageId: 'missingAssignee' }], + }, + { + code: `// @owner @aniruddha\n test.fail('should retry after failure', async ({ page }) => {});`, + errors: [{ messageId: 'missingAssignee' }], + }, + { + code: `/* @responsible @omkar */\n test.fail('known bug', async ({ page }) => {});`, + errors: [{ messageId: 'missingAssignee' }], + }, + ], +}); diff --git a/packages/docs/rules/require-test-assignee-comment.md b/packages/docs/rules/require-test-assignee-comment.md new file mode 100644 index 0000000..0f1e43f --- /dev/null +++ b/packages/docs/rules/require-test-assignee-comment.md @@ -0,0 +1,52 @@ +# Custom ESLint Rule: require-test-assignee-comment + +This rule ensures that Playwright `test.fixme` and `test.fail` annotations are never left orphaned by requiring an explicit assignee comment. + +## What it checks + +- `test.fixme(...)` +- `test.fail(...)` + +The rule reports when these calls are present without a nearby comment assigning ownership. + +## Accepted comment formats + +Valid examples: + +```js +// Assignee: @username +``` + +Only the `@assignee` syntax is accepted by this rule. + +## Example + +### ✅ Valid + +```js +// Assignee: @arjun +test.fixme('known issue', async ({ page }) => {}); +``` + +### ❌ Invalid + +```js +test.fixme('known issue', async ({ page }) => {}); +``` + +## Configuration + +```js +import fyleCore from '@fyle/eslint-plugin'; + +export default [ + { + plugins: { + '@fyle': fyleCore, + }, + rules: { + '@fyle/require-test-assignee-comment': 'error', + }, + }, +]; +```