Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat create default component test rule #1878

Merged
merged 3 commits into from
Dec 2, 2024
Merged
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
60 changes: 60 additions & 0 deletions packages/eslint-plugin-sui/docs/rules/default-component-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Detect default component test cases that haven't been customized (`default-component-test`)

This rule detects when a component's test file only contains the default test cases generated by the component generator. This indicates that the developer has created a new component but hasn't written specific tests for its functionality.

## Rule Details

The component generator creates a default test file with three basic test cases that only verify basic React component requirements. While these tests are useful as a starting point, they don't validate the specific functionality of your component.

This rule will warn you when:
- You have only default test cases in your test file
- Individual test cases that match the default generated ones

Examples of **incorrect** code for this rule:

```js
describe.context.default('MyComponent', () => {
it('should render without crashing', () => {
// Default test implementation
})

it('should NOT render null', () => {
// Default test implementation
})

it('should NOT extend classNames', () => {
// Default test implementation
})
})
```

Examples of **correct** code for this rule:

```js
describe.context.default('MyComponent', () => {
it('should render without crashing', () => {
// Default test implementation
})

it('should show error message when invalid input', () => {
// Custom test validating specific component behavior
})

it('should trigger onSubmit when form is valid', () => {
// Custom test validating specific component behavior
})
})
```

### Options

This rule has no options.

## When Not To Use It

If you are working with non-component code or if you're satisfied with the basic React component validation that the default tests provide.

## Further Reading

- [React Testing Best Practices](https://reactjs.org/docs/testing.html)
- [Component Testing Guidelines](https://github.mpi-internal.com/scmspain/es-td-agreements/blob/master/30-Frontend/00-agreements)
4 changes: 3 additions & 1 deletion packages/eslint-plugin-sui/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const DecoratorDeprecated = require('./rules/decorator-deprecated.js')
const DecoratorDeprecatedRemarkMethod = require('./rules/decorator-deprecated-remark-method.js')
const DecoratorInlineError = require('./rules/decorator-inline-error.js')
const LayersArch = require('./rules/layers-architecture.js')
const DefaultComponentTest = require('./rules/default-component-test.js')

// ------------------------------------------------------------------------------
// Plugin Definition
Expand All @@ -23,6 +24,7 @@ module.exports = {
'decorator-deprecated': DecoratorDeprecated,
'decorator-deprecated-remark-method': DecoratorDeprecatedRemarkMethod,
'decorator-inline-error': DecoratorInlineError,
'layers-arch': LayersArch
'layers-arch': LayersArch,
'default-component-test': DefaultComponentTest
}
}
93 changes: 93 additions & 0 deletions packages/eslint-plugin-sui/src/rules/default-component-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* @fileoverview Detect default component test cases that haven't been customized
*/
'use strict'

const dedent = require('string-dedent')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Detect when component test file only contains default test cases',
recommended: true,
url: 'https://github.mpi-internal.com/scmspain/es-td-agreements/blob/master/30-Frontend/00-agreements'
},
schema: [],
messages: {
defaultTestCase: dedent`
This seems to be a default test case generated by the component generator.
Please add specific test cases that validate your component's functionality.
`,
onlyDefaultTests: dedent`
This test file only contains default test cases.
Please add specific tests that validate your component's functionality.
`
}
},

create(context) {
// Track the test cases we find
const defaultTestCases = new Set([
'should render without crashing',
'should NOT render null',
'should NOT extend classNames'
])

const foundTestCases = new Set()
let totalTestCases = 0

function isComponentTestFile(filename) {
return filename.includes('/components/') && filename.endsWith('.test.js')
}

return {
Program(node) {
// Reset counters for each file
foundTestCases.clear()
totalTestCases = 0
},

CallExpression(node) {
const filename = context.getFilename()
// Solo procesar si estamos en un archivo de test de componente
if (!isComponentTestFile(filename)) return

// Look for it() calls
if (node.callee.name !== 'it') return

const testName = node.arguments[0]?.value
if (!testName) return

totalTestCases++

// Check if this is one of the default test cases
if (defaultTestCases.has(testName)) {
foundTestCases.add(testName)

context.report({
node: node.arguments[0],
messageId: 'defaultTestCase'
})
}

// After processing all tests in this block, check if they are all default ones
if (
totalTestCases > 0 &&
foundTestCases.size === defaultTestCases.size &&
[...foundTestCases].every(test => defaultTestCases.has(test))
) {
context.report({
node: node.parent,
messageId: 'onlyDefaultTests'
})
}
}
}
}
}
100 changes: 100 additions & 0 deletions packages/eslint-plugin-sui/test/server/default-component-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import dedent from 'dedent'
import {RuleTester} from 'eslint'

import rule from '../../src/rules/default-component-test.js'

const resolvedBabelPresetSui = require.resolve('babel-preset-sui')
const parser = require.resolve('@babel/eslint-parser')

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const ruleTester = new RuleTester({
parser,
parserOptions: {
babelOptions: {configFile: resolvedBabelPresetSui}
}
})

ruleTester.run('default-component-test', rule, {
valid: [
// Should ignore non-component test files
{
filename: '/domain/value-objects/test/car.test.js',
code: dedent`
describe.context.default('Car', () => {
it('should render without crashing', () => {})
it('should NOT render null', () => {})
it('should NOT extend classNames', () => {})
})
`
},
// Should be valid when there are custom tests along with default ones
{
filename: '/components/card/alert/test/index.test.js',
code: dedent`
describe.context.default('CardAlert', Component => {
it('should render without crashing', () => {})
it('should NOT render null', () => {})
it('should show custom alert message', () => {})
it('should trigger onClose callback', () => {})
})
`
},
// Should be valid when all tests are custom
{
filename: '/components/card/alert/test/index.test.js',
code: dedent`
describe.context.default('CardAlert', Component => {
it('should show title correctly', () => {})
it('should handle click events', () => {})
})
`
}
],

invalid: [
// Should detect when only default tests are present
{
filename: '/components/card/alert/test/index.test.js',
code: dedent`
describe.context.default('CardAlert', Component => {
it('should render without crashing', () => {})
it('should NOT render null', () => {})
it('should NOT extend classNames', () => {})
})
`,
errors: [
{
messageId: 'defaultTestCase',
line: 2
},
{
messageId: 'defaultTestCase',
line: 3
},
{
messageId: 'defaultTestCase',
line: 4
}
]
},
// Should detect individual default tests
{
filename: '/components/card/alert/test/index.test.js',
code: dedent`
describe.context.default('CardAlert', Component => {
it('should render without crashing', () => {})
it('should handle click events', () => {})
})
`,
errors: [
{
messageId: 'defaultTestCase',
line: 2
}
]
}
]
})
3 changes: 2 additions & 1 deletion packages/sui-lint/eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const REACT_RULES = {

const TESTING_RULES = {
'chai-friendly/no-unused-expressions': [RULES.ERROR, {allowShortCircuit: true, allowTernary: true}],
'no-only-tests/no-only-tests': RULES.ERROR
'no-only-tests/no-only-tests': RULES.ERROR,
'sui/default-component-test': RULES.WARNING
}

const JEST_TESTING_RULES = {
Expand Down
3 changes: 2 additions & 1 deletion packages/sui-lint/eslintrc.ts.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const REACT_RULES = {

const TESTING_RULES = {
'chai-friendly/no-unused-expressions': [RULES.ERROR, {allowShortCircuit: true, allowTernary: true}],
'no-only-tests/no-only-tests': RULES.ERROR
'no-only-tests/no-only-tests': RULES.ERROR,
'sui/default-component-test': RULES.WARNING
}

const JEST_TESTING_RULES = {
Expand Down