Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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';
import requireDateDayTwoDigits from './rules/require-date-day-two-digits.js';

const rules = {
'i18n-key-naming-convention': i18nKeyNamingConvention,
Expand All @@ -12,6 +14,8 @@ const rules = {
'no-inline-date-format': noInlineDateFormat,
'no-hardcoded-strings': noHardcodedStrings,
'no-date-currency-mutation': noDateCurrencyMutation,
'require-test-assignee-comment': requireTestAssigneeComment,
'require-date-day-two-digits': requireDateDayTwoDigits,
};

const configs = {
Expand Down
75 changes: 75 additions & 0 deletions packages/core/src/rules/require-date-day-two-digits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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-date-day-two-digits.md',
);

const RULE_NAME = 'require-date-day-two-digits';

export default createRule({
name: RULE_NAME,
meta: {
type: 'problem',
docs: {
description:
"Require 'day' option to use '2-digit' when calling Date.prototype.toLocaleDateString to ensure two-digit day formatting (e.g., '01 Mar' not '1 Mar').",
recommended: 'recommended',
},
schema: [],
messages: {
requireTwoDigit: "Use day: '2-digit' in toLocaleDateString options to produce two-digit day values.",
},
},
defaultOptions: [],
create(context) {
function isToLocaleDateStringCall(node) {
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
const callee = node.callee;
if (callee.type !== AST_NODE_TYPES.MemberExpression) return false;
const prop = callee.property;
if (prop.type !== AST_NODE_TYPES.Identifier) return false;
return prop.name === 'toLocaleDateString';
}

return {
CallExpression(node) {
if (!isToLocaleDateStringCall(node)) return;

// Determine which argument is the options object.
// toLocaleDateString(locales?, options?) -> options is arg[1] if arg0 is string/array, else arg[0] if it's an object
const args = node.arguments || [];
let optionsArg = null;
if (args.length >= 2) {
optionsArg = args[1];
} else if (args.length === 1) {
const first = args[0];
if (first.type === AST_NODE_TYPES.ObjectExpression) optionsArg = first;
}

if (!optionsArg) return;

// If optionsArg is not object literal, skip
if (optionsArg.type !== AST_NODE_TYPES.ObjectExpression) return;

// Find day property
for (const prop of optionsArg.properties) {
if (prop.type !== AST_NODE_TYPES.Property) continue;
const key = prop.key;
const value = prop.value;
const keyName =
key.type === AST_NODE_TYPES.Identifier
? key.name
: key.type === AST_NODE_TYPES.Literal
? String(key.value)
: null;
if (keyName === 'day') {
if (value.type === AST_NODE_TYPES.Literal && value.value === 'numeric') {
context.report({ node: value, messageId: 'requireTwoDigit' });
}
break;
}
}
},
};
},
});
67 changes: 67 additions & 0 deletions packages/core/src/rules/require-test-assignee-comment.js
Original file line number Diff line number Diff line change
@@ -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' });
}
},
};
},
});
38 changes: 38 additions & 0 deletions packages/core/src/tests/require-date-day-two-digits.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { RuleTester } from '@typescript-eslint/rule-tester';
import tsParser from '@typescript-eslint/parser';
import rule from '../rules/require-date-day-two-digits.js';

const ruleTester = new RuleTester({
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
},
});

ruleTester.run('require-date-day-two-digits', rule, {
valid: [
`new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })`,
`new Date().toLocaleDateString({ month: 'short', day: '2-digit' })`,
// no day option
`new Date().toLocaleDateString('en-US', { month: 'short', year: 'numeric' })`,
// non-literal day value (can't assert)
`const d = 'numeric'; new Date().toLocaleDateString('en-US', { day: d })`,
],
invalid: [
{
code: `new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })`,
errors: [{ messageId: 'requireTwoDigit' }],
},
{
code: `new Date().toLocaleDateString({ day: 'numeric' })`,
errors: [{ messageId: 'requireTwoDigit' }],
},
{
code: `new Date().toLocaleDateString('en-GB', { day: 'numeric' })`,
errors: [{ messageId: 'requireTwoDigit' }],
},
],
});
39 changes: 39 additions & 0 deletions packages/core/src/tests/require-test-assignee-comment.test.js
Original file line number Diff line number Diff line change
@@ -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' }],
},
],
});
27 changes: 27 additions & 0 deletions packages/docs/rules/require-date-day-two-digits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Custom ESLint Rule: require-date-day-two-digits

This rule enforces using `day: '2-digit'` when calling `Date.prototype.toLocaleDateString` so date strings always use two-digit day representations (e.g., `01 Mar` instead of `1 Mar`).

## What it checks

- Calls to `toLocaleDateString` with an object literal options argument containing `day: 'numeric'` are reported.
- Cases where `day` is missing or uses `'2-digit'` are allowed.
- If the `day` value is a non-literal (variable/expression), the rule does not attempt to assert and will not report.

## Example

### ✅ Valid

```js
new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
```

### ❌ Invalid

```js
new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
```

## Rationale

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Configuration is missing


E2E tests should match the application's date formatting which uses two-digit days. Using `day: 'numeric'` produces single-digit days for the 1st–9th days, causing intermittent failures when tests expect two-digit formatting.
52 changes: 52 additions & 0 deletions packages/docs/rules/require-test-assignee-comment.md
Original file line number Diff line number Diff line change
@@ -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',
},
},
];
```
Loading